Add .packages() to Nene.

The API is similar to .users() - Every method relating to packages must
specify the user it applies to. I also experimented with an alternative
api which looks like `testApis.forUser(user).packages()....` but it
becomes a lot more complex, as we need to maintain a set of APIs which
apply to all users and one which applies to individual users.

Bug: 180293870
Test: atest NeneTest
Change-Id: I61515f8bd0299fdd2459f97f5018b77cd97fb651
diff --git a/common/device-side/bedstead/nene/Android.bp b/common/device-side/bedstead/nene/Android.bp
index 9931b88..d21fab6 100644
--- a/common/device-side/bedstead/nene/Android.bp
+++ b/common/device-side/bedstead/nene/Android.bp
@@ -28,6 +28,16 @@
         "truth-prebuilt",
         "testng" // for assertThrows
     ],
+    data: [":NeneTestApp1"],
     manifest: "src/test/AndroidManifest.xml",
     min_sdk_version: "26"
+}
+
+android_test_helper_app {
+    name: "NeneTestApp1",
+    static_libs: [
+        "EventLib"
+    ],
+    manifest: "testapps/TestApp1.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
index 89005a5..f7a5151 100644
--- a/common/device-side/bedstead/nene/AndroidTest.xml
+++ b/common/device-side/bedstead/nene/AndroidTest.xml
@@ -19,6 +19,10 @@
         <option name="cleanup-apks" value="true" />
         <option name="test-file-name" value="NeneTest.apk" />
     </target_preparer>
+    <target_preparer class="com.android.compatibility.common.tradefed.targetprep.FilePusher">
+        <option name="cleanup" value="true" />
+        <option name="push" value="NeneTestApp1.apk->/data/local/tmp/NeneTestApp1.apk" />
+    </target_preparer>
     <test class="com.android.tradefed.testtype.AndroidJUnitTest" >
         <option name="package" value="com.android.bedstead.nene.test" />
     </test>
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
index 18b38f0..edc688b 100644
--- 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
@@ -16,17 +16,23 @@
 
 package com.android.bedstead.nene;
 
+import com.android.bedstead.nene.packages.Packages;
 import com.android.bedstead.nene.users.Users;
 
 /**
  * Entry point to Nene Test APIs.
  */
 public final class TestApis {
-
     private final Users mUsers = new Users();
+    private final Packages mPackages = new Packages(this);
 
     /** Access Test APIs related to Users. */
     public Users users() {
         return mUsers;
     }
+
+    /** Access Test APIs related to Packages. */
+    public Packages packages() {
+        return mPackages;
+    }
 }
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
index c424223..b71106b 100644
--- 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
@@ -26,7 +26,7 @@
 
     public AdbException(String message, String command, String output) {
         super(message);
-        if (command == null || output == null) {
+        if (command == null) {
             throw new NullPointerException();
         }
         this.command = command;
@@ -35,7 +35,7 @@
 
     public AdbException(String message, String command, String output, Throwable cause) {
         super(message, cause);
-        if (command == null || output == null) {
+        if (command == null) {
             throw new NullPointerException();
         }
         this.command = command;
diff --git a/common/device-side/bedstead/nene/src/main/java/com/android/bedstead/nene/packages/AdbPackageParser.java b/common/device-side/bedstead/nene/src/main/java/com/android/bedstead/nene/packages/AdbPackageParser.java
new file mode 100644
index 0000000..b70d1f9
--- /dev/null
+++ b/common/device-side/bedstead/nene/src/main/java/com/android/bedstead/nene/packages/AdbPackageParser.java
@@ -0,0 +1,46 @@
+/*
+ * 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.packages;
+
+import android.annotation.TargetApi;
+import android.os.Build;
+
+import com.android.bedstead.nene.exceptions.AdbParseException;
+
+import java.util.Map;
+import java.util.Set;
+
+/** Parser for `adb dumpsys package`. */
+@TargetApi(Build.VERSION_CODES.O)
+interface AdbPackageParser {
+
+    static AdbPackageParser get(Packages packages, int sdkVersion) {
+        return new AdbPackageParser26(packages);
+    }
+
+    /**
+     * The result of parsing.
+     *
+     * <p>Values which are not used on the current version of Android will be {@code null}.
+     */
+    class ParseResult {
+        Map<String, Package> mPackages;
+        Set<String> mFeatures;
+    }
+
+    ParseResult parse(String dumpsysPackageOutput) throws AdbParseException;
+}
diff --git a/common/device-side/bedstead/nene/src/main/java/com/android/bedstead/nene/packages/AdbPackageParser26.java b/common/device-side/bedstead/nene/src/main/java/com/android/bedstead/nene/packages/AdbPackageParser26.java
new file mode 100644
index 0000000..39485a8
--- /dev/null
+++ b/common/device-side/bedstead/nene/src/main/java/com/android/bedstead/nene/packages/AdbPackageParser26.java
@@ -0,0 +1,129 @@
+/*
+ * 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.packages;
+
+import static com.android.bedstead.nene.utils.ParserUtils.extractIndentedSections;
+
+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;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * Parser for `adb dumpsys package` on Android O+.
+ *
+ * <p>This class is structured so that future changes in ADB output can be dealt with by extending
+ * this class and overriding the appropriate section parsers.
+ */
+@RequiresApi(Build.VERSION_CODES.O)
+public class AdbPackageParser26 implements AdbPackageParser {
+
+    private static final int PACKAGE_LIST_BASE_INDENTATION = 2;
+
+    private final Packages mPackages;
+
+    AdbPackageParser26(Packages packages) {
+        if (packages == null) {
+            throw new NullPointerException();
+        }
+        mPackages = packages;
+    }
+
+    @Override
+    public ParseResult parse(String dumpsysPackageOutput) throws AdbParseException {
+        ParseResult parseResult = new ParseResult();
+        parseResult.mFeatures = parseFeatures(dumpsysPackageOutput);
+        parseResult.mPackages = parsePackages(dumpsysPackageOutput);
+        return parseResult;
+    }
+
+    Set<String> parseFeatures(String dumpsysPackageOutput) throws AdbParseException {
+        String featuresList = extractFeaturesList(dumpsysPackageOutput);
+        Set<String> features = new HashSet<>();
+        for (String featureLine : featuresList.split("\n")) {
+            features.add(featureLine.trim());
+        }
+        return features;
+    }
+
+    String extractFeaturesList(String dumpsysPackageOutput) throws AdbParseException {
+        try {
+            return dumpsysPackageOutput.split("Features:\n", 2)[1].split("\n\n", 2)[0];
+        } catch (IndexOutOfBoundsException e) {
+            throw new AdbParseException("Error extracting features list", dumpsysPackageOutput, e);
+        }
+    }
+
+    Map<String, Package> parsePackages(String dumpsysUsersOutput) throws AdbParseException {
+        String packagesList = extractPackagesList(dumpsysUsersOutput);
+
+        Set<String> packageStrings = extractPackageStrings(packagesList);
+        Map<String, Package> packages = new HashMap<>();
+        for (String packageString : packageStrings) {
+            Package pkg = new Package(mPackages, parsePackage(packageString));
+            packages.put(pkg.packageName(), pkg);
+        }
+        return packages;
+    }
+
+    String extractPackagesList(String dumpsysPackageOutput) throws AdbParseException {
+        try {
+            return dumpsysPackageOutput.split("\nPackages:\n", 2)[1].split("\n\n", 2)[0];
+        } catch (IndexOutOfBoundsException e) {
+            throw new AdbParseException("Error extracting packages list", dumpsysPackageOutput, e);
+        }
+    }
+
+    Set<String> extractPackageStrings(String packagesList) throws AdbParseException {
+        return extractIndentedSections(packagesList, PACKAGE_LIST_BASE_INDENTATION);
+    }
+
+    private static final Pattern USER_INSTALLED_PATTERN =
+            Pattern.compile("User (\\d+):.*?installed=(\\w+)");
+
+    Package.MutablePackage parsePackage(String packageString) throws AdbParseException {
+        try {
+            String packageName = packageString.split("\\[", 2)[1].split("]", 2)[0];
+            Package.MutablePackage pkg = new Package.MutablePackage();
+            pkg.mPackageName = packageName;
+            pkg.mInstalledOnUsers = new HashSet<>();
+
+
+            Matcher userInstalledMatcher = USER_INSTALLED_PATTERN.matcher(packageString);
+            while (userInstalledMatcher.find()) {
+                int userId = Integer.parseInt(userInstalledMatcher.group(1));
+                boolean isInstalled = Boolean.parseBoolean(userInstalledMatcher.group(2));
+
+                if (isInstalled) {
+                    pkg.mInstalledOnUsers.add(mPackages.mTestApis.users().find(userId));
+                }
+            }
+
+            return pkg;
+        } catch (IndexOutOfBoundsException | NumberFormatException e) {
+            throw new AdbParseException("Error parsing package", packageString, e);
+        }
+    }
+}
diff --git a/common/device-side/bedstead/nene/src/main/java/com/android/bedstead/nene/packages/Package.java b/common/device-side/bedstead/nene/src/main/java/com/android/bedstead/nene/packages/Package.java
new file mode 100644
index 0000000..41c1522
--- /dev/null
+++ b/common/device-side/bedstead/nene/src/main/java/com/android/bedstead/nene/packages/Package.java
@@ -0,0 +1,53 @@
+/*
+ * 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.packages;
+
+import com.android.bedstead.nene.users.UserReference;
+
+import java.util.Set;
+
+/**
+ * Resolved information about a package on the device.
+ */
+public class Package extends PackageReference {
+
+    static final class MutablePackage {
+        String mPackageName;
+        Set<UserReference> mInstalledOnUsers;
+    }
+
+    private final MutablePackage mMutablePackage;
+
+    Package(Packages packages, MutablePackage mutablePackage) {
+        super(packages, mutablePackage.mPackageName);
+        mMutablePackage = mutablePackage;
+    }
+
+    /** Get {@link UserReference}s who have this {@link Package} installed. */
+    public Set<UserReference> installedOnUsers() {
+        return mMutablePackage.mInstalledOnUsers;
+    }
+
+    @Override
+    public String toString() {
+        StringBuilder stringBuilder = new StringBuilder("Package{");
+        stringBuilder.append("packageName=" + mMutablePackage.mPackageName);
+        stringBuilder.append("installedOnUsers=" + mMutablePackage.mInstalledOnUsers);
+        stringBuilder.append("}");
+        return stringBuilder.toString();
+    }
+}
diff --git a/common/device-side/bedstead/nene/src/main/java/com/android/bedstead/nene/packages/PackageReference.java b/common/device-side/bedstead/nene/src/main/java/com/android/bedstead/nene/packages/PackageReference.java
new file mode 100644
index 0000000..5a49ae7
--- /dev/null
+++ b/common/device-side/bedstead/nene/src/main/java/com/android/bedstead/nene/packages/PackageReference.java
@@ -0,0 +1,115 @@
+/*
+ * 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.packages;
+
+import androidx.annotation.Nullable;
+
+import com.android.bedstead.nene.exceptions.AdbException;
+import com.android.bedstead.nene.exceptions.NeneException;
+import com.android.bedstead.nene.users.UserReference;
+import com.android.bedstead.nene.utils.ShellCommand;
+import com.android.bedstead.nene.utils.ShellCommandUtils;
+
+import java.io.File;
+
+/**
+ * A representation of a package on device which may or may not exist.
+ *
+ * <p>To resolve the package into a {@link Package}, see {@link #resolve()}.
+ */
+public abstract class PackageReference {
+
+    private final Packages mPackages;
+    private final String mPackageName;
+
+    PackageReference(Packages packages, String packageName) {
+        mPackages = packages;
+        mPackageName = packageName;
+    }
+
+    /** Return the package's name. */
+    public String packageName() {
+        return mPackageName;
+    }
+
+    /**
+     * Get the current state of the {@link Package} from the device, or {@code null} if the package
+     * does not exist.
+     */
+    @Nullable
+    public Package resolve() {
+        return mPackages.fetchPackage(mPackageName);
+    }
+
+    /**
+     * Install the package on the given user.
+     *
+     * <p>If you wish to install a package which is not already installed on another user, see
+     * {@link Packages#install(UserReference, File)}.
+     */
+    public PackageReference install(UserReference user) {
+        if (user == null) {
+            throw new NullPointerException();
+        }
+        try {
+            // Expected output "Package X installed for user: Y"
+            ShellCommand.builderForUser(user, "pm install-existing")
+                    .addOperand(mPackageName)
+                    .executeAndValidateOutput(
+                            (output) -> output.contains("installed for user"));
+            return this;
+        } catch (AdbException e) {
+            throw new NeneException("Could not install-existing package " + this, e);
+        }
+    }
+
+    /**
+     * Uninstall the package for the given user.
+     *
+     * <p>If this is the last user which has this package installed, then the package will no
+     * longer {@link #resolve()}.
+     */
+    public PackageReference uninstall(UserReference user) {
+        if (user == null) {
+            throw new NullPointerException();
+        }
+        try {
+            // Expected output "Success"
+            ShellCommand.builderForUser(user, "pm uninstall")
+                    .addOperand(mPackageName)
+                    .executeAndValidateOutput(ShellCommandUtils::startsWithSuccess);
+            return this;
+        } catch (AdbException e) {
+            throw new NeneException("Could not uninstall package " + this, e);
+        }
+    }
+
+    @Override
+    public int hashCode() {
+        return mPackageName.hashCode();
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (!(obj instanceof PackageReference)) {
+            return false;
+        }
+
+        PackageReference other = (PackageReference) obj;
+        return other.mPackageName.equals(mPackageName);
+    }
+}
diff --git a/common/device-side/bedstead/nene/src/main/java/com/android/bedstead/nene/packages/Packages.java b/common/device-side/bedstead/nene/src/main/java/com/android/bedstead/nene/packages/Packages.java
new file mode 100644
index 0000000..a10aed5
--- /dev/null
+++ b/common/device-side/bedstead/nene/src/main/java/com/android/bedstead/nene/packages/Packages.java
@@ -0,0 +1,166 @@
+/*
+ * 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.packages;
+
+import static android.os.Build.VERSION.SDK_INT;
+
+import static com.android.bedstead.nene.users.User.UserState.RUNNING_UNLOCKED;
+
+import androidx.annotation.Nullable;
+
+import com.android.bedstead.nene.TestApis;
+import com.android.bedstead.nene.exceptions.AdbException;
+import com.android.bedstead.nene.exceptions.AdbParseException;
+import com.android.bedstead.nene.exceptions.NeneException;
+import com.android.bedstead.nene.users.User;
+import com.android.bedstead.nene.users.UserReference;
+import com.android.bedstead.nene.utils.ShellCommand;
+import com.android.bedstead.nene.utils.ShellCommandUtils;
+
+import java.io.File;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Test APIs relating to packages.
+ */
+public final class Packages {
+
+    private Map<String, Package> mCachedPackages = null;
+    private Set<String> mFeatures = null;
+    private final AdbPackageParser mParser = AdbPackageParser.get(this, SDK_INT);
+    final TestApis mTestApis;
+
+    public Packages(TestApis testApis) {
+        if (testApis == null) {
+            throw new NullPointerException();
+        }
+        mTestApis = testApis;
+    }
+
+
+    /** Get the features available on the device. */
+    public Set<String> features() {
+        if (mFeatures == null) {
+            fillCache();
+        }
+
+        return mFeatures;
+    }
+
+    /** Resolve all packages on the device. */
+    public Collection<Package> all() {
+        fillCache();
+
+        return mCachedPackages.values();
+    }
+
+    /** Resolve all packages installed for a given {@link UserReference}. */
+    public Collection<Package> installedForUser(UserReference user) {
+        if (user == null) {
+            throw new NullPointerException();
+        }
+        Set<Package> installedForUser = new HashSet<>();
+
+        for (Package pkg : all()) {
+            if (pkg.installedOnUsers().contains(user)) {
+                installedForUser.add(pkg);
+            }
+        }
+
+        return installedForUser;
+    }
+
+    /**
+     * Install an APK file to a given {@link UserReference}.
+     *
+     * <p>The user must be started.
+     *
+     * <p>If the package is already installed, this will replace it.
+     */
+    public void install(UserReference user, File apkFile) {
+        if (user == null || apkFile == null) {
+            throw new NullPointerException();
+        }
+        User resolvedUser = user.resolve();
+        // TODO(scottjonathan): Consider if it's worth the additional call here - we could
+        //  optionally instead timeout the shell command (it doesn't respond if the user isn't
+        //  started)
+        if (resolvedUser == null || resolvedUser.state() != RUNNING_UNLOCKED) {
+            throw new NeneException("Packages can not be installed in non-started users "
+                    + "(Trying to install into user " + resolvedUser + ")");
+        }
+
+        // By default when using ADB we don't know the package name of the file upon success.
+        // we could make an additional call to get it (either parsing all installed and finding the
+        // one matching the apk, or by trying to install again and parsing the error - this would
+        // only work before P because after P there isn't an error) - but that
+        // would mean we are making two adb calls rather than one - needs to be decided.
+
+        try {
+            // Expected output "Success"
+            ShellCommand.builderForUser(user, "pm install")
+                    .addOperand("-r") // Reinstall automatically
+                    .addOperand(apkFile.getAbsolutePath())
+                    .executeAndValidateOutput(ShellCommandUtils::startsWithSuccess);
+        } catch (AdbException e) {
+            throw new NeneException("Could not install " + apkFile + " for user " + user, e);
+        }
+    }
+
+    @Nullable
+    Package fetchPackage(String packageName) {
+        // TODO(scottjonathan): fillCache probably does more than we need here -
+        //  can we make it more efficient?
+        fillCache();
+
+        Package pkg = mCachedPackages.get(packageName);
+        if (pkg == null || pkg.installedOnUsers().isEmpty()) {
+            return null; // Treat it as uninstalled once all users are removed/removing
+        }
+
+        return pkg;
+    }
+
+    /**
+     * Get a reference to a package with the given {@code packageName}.
+     *
+     * <p>This does not guarantee that the package exists. Call {@link PackageReference#resolve()}
+     * to find specific details about the package on the device.
+     */
+    public PackageReference find(String packageName) {
+        if (packageName == null) {
+            throw new NullPointerException();
+        }
+        return new UnresolvedPackage(this, packageName);
+    }
+
+    private void fillCache() {
+        try {
+            // TODO: Replace use of adb on supported versions of Android
+            String packageDumpsysOutput = ShellCommandUtils.executeCommand("dumpsys package");
+            AdbPackageParser.ParseResult result = mParser.parse(packageDumpsysOutput);
+
+            mCachedPackages = result.mPackages;
+            mFeatures = result.mFeatures;
+        } 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/packages/UnresolvedPackage.java b/common/device-side/bedstead/nene/src/main/java/com/android/bedstead/nene/packages/UnresolvedPackage.java
new file mode 100644
index 0000000..9bfe75c
--- /dev/null
+++ b/common/device-side/bedstead/nene/src/main/java/com/android/bedstead/nene/packages/UnresolvedPackage.java
@@ -0,0 +1,27 @@
+/*
+ * 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.packages;
+
+/**
+ * Default implementation of {@link PackageReference} used when we haven't fetched information from
+ * the device.
+ */
+public final class UnresolvedPackage extends PackageReference {
+    UnresolvedPackage(Packages packages, String packageName) {
+        super(packages, packageName);
+    }
+}
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
index 1940217..23f279b 100644
--- 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
@@ -29,7 +29,7 @@
  * Parser for the output of "adb dumpsys user".
  */
 @TargetApi(Build.VERSION_CODES.O)
-public interface AdbUserParser {
+interface AdbUserParser {
 
     static AdbUserParser get(Users users, int sdkVersion) {
         if (sdkVersion >= 30) {
@@ -48,7 +48,5 @@
         @Nullable Map<String, UserType> mUserTypes;
     }
 
-    default ParseResult parse(String dumpsysUsersOutput) throws AdbParseException {
-        throw new UnsupportedOperationException();
-    }
+    ParseResult parse(String dumpsysUsersOutput) throws AdbParseException;
 }
\ 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
index 1c5ea79..470edf0 100644
--- 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
@@ -16,6 +16,8 @@
 
 package com.android.bedstead.nene.users;
 
+import static com.android.bedstead.nene.utils.ParserUtils.extractIndentedSections;
+
 import android.os.Build;
 
 import androidx.annotation.RequiresApi;
@@ -23,7 +25,6 @@
 import com.android.bedstead.nene.exceptions.AdbParseException;
 
 import java.util.HashMap;
-import java.util.HashSet;
 import java.util.Map;
 import java.util.Set;
 
@@ -79,6 +80,9 @@
  *   Supports switchable users: false
  *   All guests ephemeral: false
  * @}
+ *
+ * <p>This class is structured so that future changes in ADB output can be dealt with by
+ *  extending this class and overriding the appropriate section parsers.
  */
 @RequiresApi(Build.VERSION_CODES.O)
 public class AdbUserParser26 implements AdbUserParser {
@@ -114,7 +118,7 @@
     String extractUsersList(String dumpsysUsersOutput) throws AdbParseException {
         try {
             return dumpsysUsersOutput.split("Users:\n", 2)[1].split("\n\n", 2)[0];
-        } catch (RuntimeException e) {
+        } catch (IndexOutOfBoundsException e) {
             throw new AdbParseException("Error extracting user list", dumpsysUsersOutput, e);
         }
     }
@@ -123,38 +127,6 @@
         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(":");
@@ -169,8 +141,9 @@
             user.mState =
                     User.UserState.fromDumpSysValue(
                             userString.split("State: ", 2)[1].split("\n", 2)[0]);
+            user.mIsRemoving = userString.contains("<removing>");
             return user;
-        } catch (RuntimeException e) {
+        } catch (IndexOutOfBoundsException 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
index 1349e03..a506e77 100644
--- 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
@@ -16,6 +16,8 @@
 
 package com.android.bedstead.nene.users;
 
+import static com.android.bedstead.nene.utils.ParserUtils.extractIndentedSections;
+
 import android.os.Build;
 
 import androidx.annotation.RequiresApi;
@@ -277,7 +279,7 @@
             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) {
+        } catch (IndexOutOfBoundsException e) {
             throw new AdbParseException("Error parsing user", userString, e);
         }
 
@@ -301,7 +303,7 @@
         try {
             return dumpsysUsersOutput.split(
                     "User types \\(\\d+ types\\):\n", 2)[1].split("\n\n", 2)[0];
-        } catch (RuntimeException e) {
+        } catch (IndexOutOfBoundsException e) {
             throw new AdbParseException("Error extracting user types list", dumpsysUsersOutput, e);
         }
     }
@@ -331,7 +333,7 @@
                     userTypeString.split("mMaxAllowedPerParent: ", 2)[1].split("\n")[0]);
 
             return userType;
-        } catch (RuntimeException e) {
+        } catch (IndexOutOfBoundsException e) {
             throw new AdbParseException("Error parsing userType", userTypeString, e);
         }
     }
diff --git a/common/device-side/bedstead/nene/src/main/java/com/android/bedstead/nene/users/UnresolvedUser.java b/common/device-side/bedstead/nene/src/main/java/com/android/bedstead/nene/users/UnresolvedUser.java
index 0396c77..725724c 100644
--- a/common/device-side/bedstead/nene/src/main/java/com/android/bedstead/nene/users/UnresolvedUser.java
+++ b/common/device-side/bedstead/nene/src/main/java/com/android/bedstead/nene/users/UnresolvedUser.java
@@ -25,4 +25,9 @@
     UnresolvedUser(Users users, int id) {
         super(users, id);
     }
+
+    @Override
+    public String toString() {
+        return "UnresolvedUser{id=" + id() + "}";
+    }
 }
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
index a82ecf9..d720740 100644
--- 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
@@ -62,6 +62,7 @@
         @Nullable Boolean mHasProfileOwner;
         @Nullable Boolean mIsPrimary;
         @Nullable UserState mState;
+        @Nullable Boolean mIsRemoving;
     }
 
     private final MutableUser mMutableUser;
@@ -86,6 +87,11 @@
         return mMutableUser.mState;
     }
 
+    /** True if the user is currently being removed. */
+    public boolean isRemoving() {
+        return mMutableUser.mIsRemoving;
+    }
+
     /**
      * Get the user type.
      *
@@ -121,6 +127,7 @@
         stringBuilder.append(", hasProfileOwner" + mMutableUser.mHasProfileOwner);
         stringBuilder.append(", isPrimary=" + mMutableUser.mIsPrimary);
         stringBuilder.append(", state=" + mMutableUser.mState);
+        stringBuilder.append("}");
         return stringBuilder.toString();
     }
 }
diff --git a/common/device-side/bedstead/nene/src/main/java/com/android/bedstead/nene/users/UserBuilder.java b/common/device-side/bedstead/nene/src/main/java/com/android/bedstead/nene/users/UserBuilder.java
index 7a95692..4eef5ef 100644
--- a/common/device-side/bedstead/nene/src/main/java/com/android/bedstead/nene/users/UserBuilder.java
+++ b/common/device-side/bedstead/nene/src/main/java/com/android/bedstead/nene/users/UserBuilder.java
@@ -26,6 +26,8 @@
 import com.android.bedstead.nene.utils.ShellCommand;
 import com.android.bedstead.nene.utils.ShellCommandUtils;
 
+import java.util.UUID;
+
 /**
  * Builder for creating a new Android User.
  */
@@ -56,7 +58,7 @@
     /** Create the user. */
     public UserReference create() {
         if (mName == null) {
-            throw new IllegalStateException("Name must be specified for a new user");
+            mName = UUID.randomUUID().toString();
         }
 
         ShellCommand.Builder commandBuilder = ShellCommand.builder("pm create-user");
diff --git a/common/device-side/bedstead/nene/src/main/java/com/android/bedstead/nene/users/UserReference.java b/common/device-side/bedstead/nene/src/main/java/com/android/bedstead/nene/users/UserReference.java
index ed0755e..5c21aef 100644
--- a/common/device-side/bedstead/nene/src/main/java/com/android/bedstead/nene/users/UserReference.java
+++ b/common/device-side/bedstead/nene/src/main/java/com/android/bedstead/nene/users/UserReference.java
@@ -86,9 +86,8 @@
             ShellCommand.builder("pm remove-user")
                     .addOperand(mId)
                     .executeAndValidateOutput(ShellCommandUtils::startsWithSuccess);
-            ShellCommandUtils.executeCommandUntilOutputValid("dumpsys user",
-                    (output) -> !output.contains("UserInfo{" + mId + ":"));
-        } catch (AdbException | InterruptedException e) {
+            mUsers.waitForUserToNotExistOrMatch(this, User::isRemoving);
+        } catch (AdbException e) {
             throw new NeneException("Could not remove user + " + this, e);
         }
     }
@@ -174,4 +173,20 @@
 
         return this;
     }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (!(obj instanceof UserReference)) {
+            return false;
+        }
+
+        UserReference other = (UserReference) obj;
+
+        return other.id() == id();
+    }
+
+    @Override
+    public int hashCode() {
+        return id();
+    }
 }
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
index bf1affa..7f52a42 100644
--- 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
@@ -17,8 +17,10 @@
 package com.android.bedstead.nene.users;
 
 import static android.os.Build.VERSION.SDK_INT;
+import static android.os.Process.myUserHandle;
 
 import android.os.Build;
+import android.os.UserHandle;
 
 import androidx.annotation.Nullable;
 import androidx.annotation.RequiresApi;
@@ -49,6 +51,11 @@
         return mCachedUsers.values();
     }
 
+    /** Get a {@link UserReference} for the user running the current test process. */
+    public UserReference instrumented() {
+        return find(myUserHandle());
+    }
+
     /** Get a {@link UserReference} for the system user. */
     public UserReference system() {
         return find(0);
@@ -59,6 +66,11 @@
         return new UnresolvedUser(this, id);
     }
 
+    /** Get a {@link UserReference} by {@code userHandle}. */
+    public UserReference find(UserHandle userHandle) {
+        return new UnresolvedUser(this, userHandle.getIdentifier());
+    }
+
     @Nullable
     User fetchUser(int id) {
         // TODO(scottjonathan): fillCache probably does more than we need here -
@@ -111,6 +123,11 @@
 
             mCachedUsers = result.mUsers;
             mCachedUserTypes = result.mUserTypes;
+
+            // We don't expose users who are currently being removed
+            mCachedUsers.entrySet().removeIf(
+                    integerUserEntry -> integerUserEntry.getValue().isRemoving());
+
         } catch (AdbException | AdbParseException e) {
             throw new RuntimeException("Error filling cache", e);
         }
@@ -138,7 +155,7 @@
     }
 
     @Nullable
-    User waitForUserToMatch(
+    private User waitForUserToMatch(
             UserReference userReference, Function<User, Boolean> userChecker,
             boolean waitForExist) {
         // TODO(scottjonathan): This is pretty heavy because we resolve everything when we know we
diff --git a/common/device-side/bedstead/nene/src/main/java/com/android/bedstead/nene/utils/ParserUtils.java b/common/device-side/bedstead/nene/src/main/java/com/android/bedstead/nene/utils/ParserUtils.java
new file mode 100644
index 0000000..42019eb
--- /dev/null
+++ b/common/device-side/bedstead/nene/src/main/java/com/android/bedstead/nene/utils/ParserUtils.java
@@ -0,0 +1,81 @@
+/*
+ * 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 com.android.bedstead.nene.exceptions.AdbParseException;
+
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * Utilities for parsing adb output.
+ */
+public final class ParserUtils {
+    private ParserUtils() {
+
+    }
+
+    /**
+     * When passed a list of sections, which are organised using significant whitespace, split
+     * them into a separate string per section.
+     *
+     * <p>For example:
+     * {@code
+     * section1
+     *     a
+     *         alpha
+     *         beta
+     *     b
+     *     c
+     * section2
+     *     a2
+     *     b2
+     * }
+     *
+     * <p>Using {@link #extractIndentedSections(String, int)} with this string, and a
+     * {@code baseIndentation} of 0 (because there are no spaces before the "section" headings)
+     * would return two strings, one from "section1" to "c" and one from "section2" to "b2".
+     */
+    public static Set<String> extractIndentedSections(String list, int baseIndentation)
+            throws AdbParseException {
+        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;
+    }
+
+    private static int countIndentation(String s) {
+        String trimmed = s.trim();
+        if (trimmed.isEmpty()) {
+            return s.length();
+        }
+        return s.indexOf(trimmed);
+    }
+}
diff --git a/common/device-side/bedstead/nene/src/main/java/com/android/bedstead/nene/utils/ShellCommand.java b/common/device-side/bedstead/nene/src/main/java/com/android/bedstead/nene/utils/ShellCommand.java
index 577d25a..0ca0044 100644
--- a/common/device-side/bedstead/nene/src/main/java/com/android/bedstead/nene/utils/ShellCommand.java
+++ b/common/device-side/bedstead/nene/src/main/java/com/android/bedstead/nene/utils/ShellCommand.java
@@ -16,7 +16,10 @@
 
 package com.android.bedstead.nene.utils;
 
+import androidx.annotation.Nullable;
+
 import com.android.bedstead.nene.exceptions.AdbException;
+import com.android.bedstead.nene.users.UserReference;
 
 import java.util.function.Function;
 
@@ -32,8 +35,21 @@
         return new Builder(command);
     }
 
+    /**
+     * Create a builder and if {@code userReference} is not {@code null}, add "--user <userId>".
+     */
+    public static Builder builderForUser(@Nullable UserReference userReference, String command) {
+        Builder builder = builder(command);
+        if (userReference != null) {
+            builder.addOption("--user", userReference.id());
+        }
+
+        return builder;
+    }
+
     public static final class Builder {
         private final StringBuilder commandBuilder;
+        private byte[] mStdInBytes = null;
         private boolean mAllowEmptyOutput = false;
 
         private Builder(String command) {
@@ -80,7 +96,8 @@
         /** See {@link ShellCommandUtils#executeCommand(java.lang.String)}. */
         public String execute() throws AdbException {
             return ShellCommandUtils.executeCommand(
-                    commandBuilder.toString(), /* allowEmptyOutput= */ mAllowEmptyOutput);
+                    commandBuilder.toString(),
+                    /* allowEmptyOutput= */ mAllowEmptyOutput);
         }
 
         /** See {@link ShellCommandUtils#executeCommandAndValidateOutput(String, Function)}. */
@@ -88,7 +105,8 @@
                 throws AdbException {
             return ShellCommandUtils.executeCommandAndValidateOutput(
                     commandBuilder.toString(),
-                    /* allowEmptyOutput= */ mAllowEmptyOutput, outputSuccessChecker);
+                    /* allowEmptyOutput= */ mAllowEmptyOutput,
+                    outputSuccessChecker);
         }
     }
 }
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
index 2c24179..d9c7da7 100644
--- 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
@@ -18,8 +18,6 @@
 
 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;
 
@@ -53,7 +51,7 @@
 
     static String executeCommand(String command, boolean allowEmptyOutput)
             throws AdbException {
-        if (SDK_INT < Build.VERSION_CODES.S) {
+        if (SDK_INT < 31) { // TODO(scottjonathan): Replace with S
             return executeCommandPreS(command, allowEmptyOutput);
         }
 
@@ -79,11 +77,9 @@
      */
     public static String executeCommandAndValidateOutput(
             String command, Function<String, Boolean> outputSuccessChecker) throws AdbException {
-        String output = executeCommand(command);
-        if (!outputSuccessChecker.apply(output)) {
-            throw new AdbException("Command did not meet success criteria", command, output);
-        }
-        return output;
+        return executeCommandAndValidateOutput(command,
+                /* allowEmptyOutput= */ false,
+                outputSuccessChecker);
     }
 
     static String executeCommandAndValidateOutput(
@@ -139,8 +135,9 @@
         return !output.toUpperCase().startsWith("ERROR");
     }
 
-    private static String executeCommandPreS(String command, boolean allowEmptyOutput)
-            throws AdbException {
+    private static String executeCommandPreS(
+            String command, boolean allowEmptyOutput) throws AdbException {
+
         String result = SystemUtil.runShellCommand(command);
 
         if (!allowEmptyOutput && result.isEmpty()) {
diff --git a/common/device-side/bedstead/nene/src/test/java/com/android/bedstead/nene/packages/PackageReferenceTest.java b/common/device-side/bedstead/nene/src/test/java/com/android/bedstead/nene/packages/PackageReferenceTest.java
new file mode 100644
index 0000000..5cbde10
--- /dev/null
+++ b/common/device-side/bedstead/nene/src/test/java/com/android/bedstead/nene/packages/PackageReferenceTest.java
@@ -0,0 +1,112 @@
+/*
+ * 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.packages;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.testng.Assert.assertThrows;
+
+import com.android.bedstead.nene.TestApis;
+import com.android.bedstead.nene.exceptions.NeneException;
+import com.android.bedstead.nene.users.UserReference;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import java.io.File;
+
+@RunWith(JUnit4.class)
+public class PackageReferenceTest {
+
+    private final TestApis mTestApis = new TestApis();
+    private final UserReference mUser = mTestApis.users().instrumented();
+    private static final String NON_EXISTING_PACKAGE_NAME = "com.package.does.not.exist";
+    private static final String PACKAGE_NAME = NON_EXISTING_PACKAGE_NAME;
+    private static final String EXISTING_PACKAGE_NAME = "com.android.providers.telephony";
+
+    // Controlled by AndroidTest.xml
+    private static final String TEST_APP_PACKAGE_NAME =
+            "com.android.bedstead.nene.testapps.TestApp1";
+    private static final File TEST_APP_APK_FILE = new File("/data/local/tmp/NeneTestApp1.apk");
+    private static final File NON_EXISTING_APK_FILE =
+            new File("/data/local/tmp/ThisApkDoesNotExist.apk");
+
+    @Test
+    public void packageName_returnsPackageName() {
+        mTestApis.packages().find(PACKAGE_NAME).packageName().equals(PACKAGE_NAME);
+    }
+
+    @Test
+    public void resolve_nonExistingPackage_returnsNull() {
+        assertThat(mTestApis.packages().find(NON_EXISTING_PACKAGE_NAME).resolve()).isNull();
+    }
+
+    @Test
+    public void resolve_existingPackage_returnsPackage() {
+        assertThat(mTestApis.packages().find(EXISTING_PACKAGE_NAME).resolve()).isNotNull();
+    }
+
+    @Test
+    public void install_packageIsInstalled() {
+        mTestApis.packages().install(mUser, TEST_APP_APK_FILE);
+        PackageReference packageReference = mTestApis.packages().find(TEST_APP_PACKAGE_NAME);
+
+        try {
+            assertThat(packageReference.resolve().installedOnUsers()).contains(mUser);
+        } finally {
+            packageReference.uninstall(mUser);
+        }
+    }
+
+    @Test
+    public void install_nonExistingPackage_throwsException() {
+        assertThrows(NeneException.class,
+                () -> mTestApis.packages().install(mUser, NON_EXISTING_APK_FILE));
+    }
+
+    @Test
+    public void uninstall_packageIsUninstalled() {
+        mTestApis.packages().install(mUser, TEST_APP_APK_FILE);
+        PackageReference packageReference = mTestApis.packages().find(TEST_APP_PACKAGE_NAME);
+
+        packageReference.uninstall(mUser);
+
+        assertThat(packageReference.resolve()).isNull();
+    }
+
+    @Test
+    public void uninstall_packageNotInstalledForUser_throwsException() {
+        UserReference otherUser = mTestApis.users().createUser().createAndStart();
+        mTestApis.packages().install(mUser, TEST_APP_APK_FILE);
+        PackageReference packageReference = mTestApis.packages().find(TEST_APP_PACKAGE_NAME);
+
+        try {
+            assertThrows(NeneException.class, () -> packageReference.uninstall(otherUser));
+        } finally {
+            packageReference.uninstall(mUser);
+            otherUser.remove();
+        }
+    }
+
+    @Test
+    public void uninstall_packageDoesNotExist_throwsException() {
+        PackageReference packageReference = mTestApis.packages().find(NON_EXISTING_PACKAGE_NAME);
+
+        assertThrows(NeneException.class, () -> packageReference.uninstall(mUser));
+    }
+}
diff --git a/common/device-side/bedstead/nene/src/test/java/com/android/bedstead/nene/packages/PackageTest.java b/common/device-side/bedstead/nene/src/test/java/com/android/bedstead/nene/packages/PackageTest.java
new file mode 100644
index 0000000..f470c8b
--- /dev/null
+++ b/common/device-side/bedstead/nene/src/test/java/com/android/bedstead/nene/packages/PackageTest.java
@@ -0,0 +1,66 @@
+/*
+ * 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.packages;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.android.bedstead.nene.TestApis;
+import com.android.bedstead.nene.users.UserReference;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import java.io.File;
+
+@RunWith(JUnit4.class)
+public class PackageTest {
+
+    // Controlled by AndroidTest.xml
+    private static final String TEST_APP_PACKAGE_NAME =
+            "com.android.bedstead.nene.testapps.TestApp1";
+    private static final File TEST_APP_APK_FILE = new File("/data/local/tmp/NeneTestApp1.apk");
+
+    private final TestApis mTestApis = new TestApis();
+    private final UserReference mUser = mTestApis.users().instrumented();
+
+    @Test
+    public void installedOnUsers_includesUserWithPackageInstalled() {
+        mTestApis.packages().install(mUser, TEST_APP_APK_FILE);
+        PackageReference packageReference = mTestApis.packages().find(TEST_APP_PACKAGE_NAME);
+
+        try {
+            assertThat(packageReference.resolve().installedOnUsers()).contains(mUser);
+        } finally {
+            packageReference.uninstall(mUser);
+        }
+    }
+
+    @Test
+    public void installedOnUsers_doesNotIncludeUserWithoutPackageInstalled() {
+        UserReference user = mTestApis.users().createUser().create();
+        mTestApis.packages().install(mUser, TEST_APP_APK_FILE);
+        PackageReference packageReference = mTestApis.packages().find(TEST_APP_PACKAGE_NAME);
+
+        try {
+            assertThat(packageReference.resolve().installedOnUsers()).doesNotContain(user);
+        } finally {
+            packageReference.uninstall(mUser);
+            user.remove();
+        }
+    }
+}
diff --git a/common/device-side/bedstead/nene/src/test/java/com/android/bedstead/nene/packages/PackagesTest.java b/common/device-side/bedstead/nene/src/test/java/com/android/bedstead/nene/packages/PackagesTest.java
new file mode 100644
index 0000000..bd09b7e7
--- /dev/null
+++ b/common/device-side/bedstead/nene/src/test/java/com/android/bedstead/nene/packages/PackagesTest.java
@@ -0,0 +1,202 @@
+/*
+ * 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.packages;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.testng.Assert.assertThrows;
+
+import com.android.bedstead.nene.TestApis;
+import com.android.bedstead.nene.exceptions.NeneException;
+import com.android.bedstead.nene.users.UserReference;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import java.io.File;
+
+@RunWith(JUnit4.class)
+public class PackagesTest {
+    private static final String INPUT_METHODS_FEATURE = "android.software.input_methods";
+    private static final String NON_EXISTING_PACKAGE = "com.package.does.not.exist";
+
+    // Controlled by AndroidTest.xml
+    private static final String TEST_APP_PACKAGE_NAME =
+            "com.android.bedstead.nene.testapps.TestApp1";
+    private static final File TEST_APP_APK_FILE = new File("/data/local/tmp/NeneTestApp1.apk");
+
+    private final TestApis mTestApis = new TestApis();
+    private final UserReference mUser = mTestApis.users().instrumented();
+    private final PackageReference mExistingPackage =
+            mTestApis.packages().find("com.android.providers.telephony");
+    private final UserReference mNonExistingUser = mTestApis.users().find(99999);
+    private final File mApkFile = new File("");
+
+    @Test
+    public void construct_nullTestApis_throwsException() {
+        assertThrows(NullPointerException.class, () -> new Packages(/* testApis= */ null));
+    }
+
+    @Test
+    public void construct_constructs() {
+        new Packages(mTestApis); // Doesn't throw any exceptions
+    }
+
+    @Test
+    public void features_noUserSpecified_containsKnownFeature() {
+        assertThat(mTestApis.packages().features()).contains(INPUT_METHODS_FEATURE);
+    }
+
+    @Test
+    public void all_containsKnownPackage() {
+        assertThat(mTestApis.packages().all()).contains(mExistingPackage);
+    }
+
+    @Test
+    public void find_nullPackageName_throwsException() {
+        assertThrows(NullPointerException.class, () -> mTestApis.packages().find(null));
+    }
+
+    @Test
+    public void find_existingPackage_returnsPackageReference() {
+        assertThat(mTestApis.packages().find(mExistingPackage.packageName())).isNotNull();
+    }
+
+    @Test
+    public void find_nonExistingPackage_returnsPackageReference() {
+        assertThat(mTestApis.packages().find(NON_EXISTING_PACKAGE)).isNotNull();
+    }
+
+    @Test
+    public void installedForUser_nullUserReference_throwsException() {
+        assertThrows(NullPointerException.class,
+                () -> mTestApis.packages().installedForUser(/* user= */ null));
+    }
+
+    @Test
+    public void installedForUser_containsPackageInstalledForUser() {
+        mTestApis.packages().install(mUser, TEST_APP_APK_FILE);
+        PackageReference packageReference = mTestApis.packages().find(TEST_APP_PACKAGE_NAME);
+
+        try {
+            assertThat(mTestApis.packages().installedForUser(mUser)).contains(packageReference);
+        } finally {
+            packageReference.uninstall(mUser);
+        }
+    }
+
+    @Test
+    public void installedForUser_doesNotContainPackageNotInstalledForUser() {
+        UserReference otherUser = mTestApis.users().createUser().create();
+        mTestApis.packages().install(mUser, TEST_APP_APK_FILE);
+        PackageReference packageReference = mTestApis.packages().find(TEST_APP_PACKAGE_NAME);
+
+        try {
+            assertThat(mTestApis.packages().installedForUser(otherUser))
+                    .doesNotContain(packageReference);
+        } finally {
+            packageReference.uninstall(mUser);
+            otherUser.remove();
+        }
+    }
+
+    @Test
+    public void install_nullUser_throwsException() {
+        assertThrows(NullPointerException.class,
+                () -> mTestApis.packages().install(/* user= */ null, mApkFile));
+    }
+
+    @Test
+    public void install_nullApkFile_throwsException() {
+        assertThrows(NullPointerException.class,
+                () -> mTestApis.packages().install(mUser, (File) /* apkFile= */ null));
+    }
+
+    @Test
+    public void install_instrumentedUser_isInstalled() {
+        mTestApis.packages().install(mTestApis.users().instrumented(), TEST_APP_APK_FILE);
+        PackageReference packageReference = mTestApis.packages().find(TEST_APP_PACKAGE_NAME);
+
+        try {
+            assertThat(packageReference.resolve().installedOnUsers())
+                    .contains(mTestApis.users().instrumented());
+        } finally {
+            packageReference.uninstall(mTestApis.users().instrumented());
+        }
+    }
+
+    @Test
+    public void install_differentUser_isInstalled() {
+        UserReference user = mTestApis.users().createUser().createAndStart();
+        mTestApis.packages().install(user, TEST_APP_APK_FILE);
+        PackageReference packageReference = mTestApis.packages().find(TEST_APP_PACKAGE_NAME);
+
+        try {
+            assertThat(packageReference.resolve().installedOnUsers()).contains(user);
+        } finally {
+            user.remove();
+        }
+    }
+
+    @Test
+    public void install_userNotStarted_throwsException() {
+        UserReference user = mTestApis.users().createUser().create().stop();
+
+        try {
+            assertThrows(NeneException.class, () -> mTestApis.packages().install(user,
+                    TEST_APP_APK_FILE));
+        } finally {
+            user.remove();
+        }
+    }
+
+    @Test
+    public void install_userDoesNotExist_throwsException() {
+        assertThrows(NeneException.class, () -> mTestApis.packages().install(mNonExistingUser,
+                TEST_APP_APK_FILE));
+    }
+
+    @Test
+    public void install_alreadyInstalledForUser_installs() {
+        mTestApis.packages().install(mUser, TEST_APP_APK_FILE);
+        PackageReference packageReference = mTestApis.packages().find(TEST_APP_PACKAGE_NAME);
+
+        try {
+            mTestApis.packages().install(mUser, TEST_APP_APK_FILE);
+            assertThat(packageReference.resolve().installedOnUsers()).contains(mUser);
+        } finally {
+            packageReference.uninstall(mUser);
+        }
+    }
+
+    @Test
+    public void install_alreadyInstalledOnOtherUser_installs() {
+        UserReference otherUser = mTestApis.users().createUser().createAndStart();
+        PackageReference packageReference = mTestApis.packages().find(TEST_APP_PACKAGE_NAME);
+        try {
+            mTestApis.packages().install(otherUser, TEST_APP_APK_FILE);
+
+            mTestApis.packages().install(mUser, TEST_APP_APK_FILE);
+
+            assertThat(packageReference.resolve().installedOnUsers()).contains(mUser);
+        } finally {
+            packageReference.uninstall(mUser);
+            otherUser.remove();
+        }
+    }
+}
diff --git a/common/device-side/bedstead/nene/src/test/java/com/android/bedstead/nene/users/UserReferenceTest.java b/common/device-side/bedstead/nene/src/test/java/com/android/bedstead/nene/users/UserReferenceTest.java
index 62fca68..4d143d6 100644
--- a/common/device-side/bedstead/nene/src/test/java/com/android/bedstead/nene/users/UserReferenceTest.java
+++ b/common/device-side/bedstead/nene/src/test/java/com/android/bedstead/nene/users/UserReferenceTest.java
@@ -31,8 +31,8 @@
 
 import androidx.test.platform.app.InstrumentationRegistry;
 
+import com.android.bedstead.nene.TestApis;
 import com.android.bedstead.nene.exceptions.NeneException;
-import com.android.bedstead.nene.utils.ShellCommand;
 import com.android.compatibility.common.util.SystemUtil;
 import com.android.eventlib.EventLogs;
 import com.android.eventlib.events.activities.ActivityCreatedEvent;
@@ -50,7 +50,8 @@
             InstrumentationRegistry.getInstrumentation().getContext();
     private static final String TEST_ACTIVITY_NAME = "com.android.bedstead.nene.test.Activity";
 
-    private final Users mUsers = new Users();
+    private final TestApis mTestApis = new TestApis();
+    private final Users mUsers = mTestApis.users();
 
     @Test
     public void id_returnsId() {
@@ -69,7 +70,7 @@
 
     @Test
     public void resolve_doesExist_returnsUser() {
-        UserReference userReference = createUser();
+        UserReference userReference = mUsers.createUser().create();
 
         try {
             assertThat(userReference.resolve()).isNotNull();
@@ -97,7 +98,7 @@
 
     @Test
     public void remove_userExists_removesUser() {
-        UserReference user = createUser();
+        UserReference user = mUsers.createUser().create();
 
         user.remove();
 
@@ -111,8 +112,7 @@
 
     @Test
     public void start_userNotStarted_userIsStarted() {
-        UserReference user = createUser()
-                .start();
+        UserReference user = mUsers.createUser().create().stop();
 
         user.start();
 
@@ -125,8 +125,7 @@
 
     @Test
     public void start_userAlreadyStarted_doesNothing() {
-        UserReference user = createUser()
-                .start();
+        UserReference user = mUsers.createUser().createAndStart();
 
         user.start();
 
@@ -144,8 +143,7 @@
 
     @Test
     public void stop_userStarted_userIsStopped() {
-        UserReference user = createUser()
-                .start();
+        UserReference user = mUsers.createUser().createAndStart();
 
         user.stop();
 
@@ -158,8 +156,7 @@
 
     @Test
     public void stop_userNotStarted_doesNothing() {
-        UserReference user = createUser()
-                .stop();
+        UserReference user = mUsers.createUser().create().stop();
 
         user.stop();
 
@@ -173,22 +170,15 @@
     @Test
     public void switchTo_userIsSwitched() throws Exception {
         assumeTrue(
-                "Install-existing only works for P+", SDK_INT >= Build.VERSION_CODES.P);
-        // TODO(scottjonathan): Might be a way of faking install-existing on older
-        //  versions (fetch the pkg and reinstall?)
-        assumeTrue(
                 "Adopting Shell Permissions only works for Q+", SDK_INT >= Build.VERSION_CODES.Q);
         // TODO(scottjonathan): In this case we can probably grant the permission through adb?
 
-        UserReference user = createUser().start();
+        UserReference user = mUsers.createUser().createAndStart();
         try {
             SystemUtil.runWithShellPermissionIdentity(() -> {
                 // for INTERACT_ACROSS_USERS
-                ShellCommand.builder("pm install-existing")
-                        .addOption("--user", user.id())
-                        .addOperand(sContext.getPackageName())
-                        .executeAndValidateOutput(
-                                (output) -> output.contains("installed for user"));
+
+                mTestApis.packages().find(sContext.getPackageName()).install(user);
                 user.switchTo();
 
                 Intent intent = new Intent();
@@ -208,8 +198,4 @@
             user.remove();
         }
     }
-
-    private UserReference createUser() {
-        return mUsers.createUser().name(USER_NAME).create();
-    }
 }
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
index e0d49de..a47095f 100644
--- 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
@@ -20,11 +20,10 @@
 
 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 android.os.UserHandle;
 
 import com.android.bedstead.nene.exceptions.AdbException;
 import com.android.bedstead.nene.utils.ShellCommandUtils;
@@ -178,6 +177,11 @@
     }
 
     @Test
+    public void find_fromUserHandle_referencesCorrectId() {
+        assertThat(mUsers.find(UserHandle.of(USER_ID)).id()).isEqualTo(USER_ID);
+    }
+
+    @Test
     public void find_constructedReferenceReferencesCorrectId() {
         assertThat(mUsers.find(USER_ID).id()).isEqualTo(USER_ID);
     }
@@ -185,7 +189,6 @@
     @Test
     public void createUser_userIsCreated()  {
         UserReference userReference = mUsers.createUser()
-                .name(USER_NAME) // required
                 .create();
 
         try {
@@ -215,7 +218,6 @@
 
         UserType type = mUsers.supportedType(RESTRICTED_USER_TYPE);
         UserReference userReference = mUsers.createUser()
-                .name(USER_NAME) // required
                 .type(type)
                 .create();
 
@@ -227,13 +229,6 @@
     }
 
     @Test
-    public void createUser_doesNotSpecifyName_throwsIllegalStateException() {
-        UserBuilder userBuilder = mUsers.createUser();
-
-        assertThrows(IllegalStateException.class, userBuilder::create);
-    }
-
-    @Test
     public void createAndStart_isStarted() {
         User user = null;
 
diff --git a/common/device-side/bedstead/nene/testapps/TestApp1.xml b/common/device-side/bedstead/nene/testapps/TestApp1.xml
new file mode 100644
index 0000000..5772b0d
--- /dev/null
+++ b/common/device-side/bedstead/nene/testapps/TestApp1.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.
+  -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+          package="com.android.bedstead.nene.testapps.TestApp1">
+    <application
+        android:appComponentFactory="com.android.eventlib.premade.EventLibAppComponentFactory">
+    </application>
+    <uses-sdk android:minSdkVersion="26" android:targetSdkVersion="26"/>
+</manifest>