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>