blob: 5c72efda67d6ecb41fc10ed7173ddf6a26f90725 [file] [log] [blame]
/*
* 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.Manifest.permission.INSTALL_PACKAGES;
import static android.Manifest.permission.INTERACT_ACROSS_USERS;
import static android.Manifest.permission.INTERACT_ACROSS_USERS_FULL;
import static android.content.Intent.ACTION_VIEW;
import static android.content.pm.PackageInstaller.EXTRA_PACKAGE_NAME;
import static android.content.pm.PackageInstaller.EXTRA_STATUS;
import static android.content.pm.PackageInstaller.EXTRA_STATUS_MESSAGE;
import static android.content.pm.PackageInstaller.STATUS_FAILURE;
import static android.content.pm.PackageInstaller.STATUS_SUCCESS;
import static android.content.pm.PackageInstaller.SessionParams.MODE_FULL_INSTALL;
import static android.content.pm.PackageManager.MATCH_ALL;
import static android.content.pm.PackageManager.MATCH_DEFAULT_ONLY;
import static android.os.Build.VERSION.SDK_INT;
import static android.os.Build.VERSION_CODES.R;
import static com.android.bedstead.nene.permissions.CommonPermissions.INSTALL_TEST_ONLY_PACKAGE;
import static com.android.bedstead.nene.permissions.CommonPermissions.USE_SYSTEM_DATA_LOADERS;
import static com.android.compatibility.common.util.FileUtils.readInputStreamFully;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.FeatureInfo;
import android.content.pm.PackageInstaller;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.content.res.Resources;
import android.net.Uri;
import android.os.Build;
import android.util.Log;
import androidx.annotation.CheckResult;
import androidx.annotation.RequiresApi;
import com.android.bedstead.nene.TestApis;
import com.android.bedstead.nene.activities.ActivityReference;
import com.android.bedstead.nene.annotations.Experimental;
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.permissions.PermissionContext;
import com.android.bedstead.nene.users.UserReference;
import com.android.bedstead.nene.utils.BlockingIntentSender;
import com.android.bedstead.nene.utils.Poll;
import com.android.bedstead.nene.utils.ResolveInfoWrapper;
import com.android.bedstead.nene.utils.ShellCommand;
import com.android.bedstead.nene.utils.ShellCommandUtils;
import com.android.bedstead.nene.utils.UndoableContext;
import com.android.bedstead.nene.utils.Versions;
import com.android.compatibility.common.util.BlockingBroadcastReceiver;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.time.Duration;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
import javax.annotation.Nullable;
/**
* Test APIs relating to packages.
*/
public final class Packages {
private static final String LOG_TAG = "Packages";
/** Reference to a Java resource. */
public static final class JavaResource {
private final String mName;
private JavaResource(String name) {
mName = name;
}
/** Reference a Java resource by name. */
public static JavaResource javaResource(String name) {
if (name == null) {
throw new NullPointerException();
}
return new JavaResource(name);
}
@Override
public String toString() {
return "JavaResource{name=" + mName + "}";
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof JavaResource)) return false;
JavaResource that = (JavaResource) o;
return mName.equals(that.mName);
}
@Override
public int hashCode() {
return Objects.hash(mName);
}
}
/** Reference to an Android resource. */
public static final class AndroidResource {
private final String mName;
private AndroidResource(String name) {
if (name == null) {
throw new NullPointerException();
}
mName = name;
}
/** Reference an Android resource by name. */
public static AndroidResource androidResource(String name) {
return new AndroidResource(name);
}
@Override
public String toString() {
return "AndroidResource{name=" + mName + "}";
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof AndroidResource)) return false;
AndroidResource that = (AndroidResource) o;
return mName.equals(that.mName);
}
@Override
public int hashCode() {
return Objects.hash(mName);
}
}
public static final Packages sInstance = new Packages();
private static final String PACKAGE_VERIFIER_INCLUDE_ADB = "verifier_verify_adb_installs";
private Set<String> mFeatures = null;
private final Context mInstrumentedContext;
private final IntentFilter mPackageAddedIntentFilter =
new IntentFilter(Intent.ACTION_PACKAGE_ADDED);
private static final PackageManager sPackageManager =
TestApis.context().instrumentedContext().getPackageManager();
static final AdbPackageParser sParser = AdbPackageParser.get(SDK_INT);
public Packages() {
mPackageAddedIntentFilter.addDataScheme("package");
mInstrumentedContext = TestApis.context().instrumentedContext();
}
/** Get the features available on the device. */
public Set<String> features() {
if (mFeatures == null) {
mFeatures = new HashSet<>();
PackageManager pm = TestApis.context().instrumentedContext().getPackageManager();
FeatureInfo[] features = pm.getSystemAvailableFeatures();
if (features != null) {
Arrays.stream(features).map(f -> f.name).forEach(mFeatures::add);
}
}
return mFeatures;
}
/** Get packages installed for the instrumented user. */
public Collection<Package> installedForUser() {
return installedForUser(TestApis.users().instrumented());
}
/**
* Resolve all packages installed for a given {@link UserReference}.
*/
public Collection<Package> installedForUser(UserReference user) {
if (user == null) {
throw new NullPointerException();
}
if (!Versions.meetsMinimumSdkVersionRequirement(R)
|| TestApis.packages().instrumented().isInstantApp()) {
AdbPackageParser.ParseResult packages = parseDumpsys();
return packages.mPackages.values().stream()
.filter(p -> p.installedOnUsers().contains(user))
.map(p -> find(p.packageName()))
.collect(Collectors.toSet());
}
if (user.equals(TestApis.users().instrumented())) {
return TestApis.context().instrumentedContext().getPackageManager()
.getInstalledPackages(/* flags= */ 0)
.stream()
.map(i -> new Package(i.packageName))
.collect(Collectors.toSet());
}
try (PermissionContext p = TestApis.permissions()
.withPermission(INTERACT_ACROSS_USERS_FULL)) {
return TestApis.context().androidContextAsUser(user).getPackageManager()
.getInstalledPackages(/* flags= */ 0)
.stream()
.map(i -> new Package(i.packageName))
.collect(Collectors.toSet());
}
}
/** Install the {@link File} to the instrumented user. */
public Package install(File apkFile) {
return install(TestApis.users().instrumented(), apkFile);
}
/** Install a file as a byte array to the instrumented user. */
public Package install(byte[] apkFile) {
return install(TestApis.users().instrumented(), apkFile);
}
/**
* 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.
*
* <p>If the package is marked testOnly, it will still be installed.
*
* <p>On versions of Android prior to Q, this will return null. On other versions it will return
* the installed package.
*/
@Nullable
public Package install(UserReference user, File apkFile) {
if (user == null || apkFile == null) {
throw new NullPointerException();
}
if (Versions.meetsMinimumSdkVersionRequirement(Build.VERSION_CODES.S)) {
return install(user, loadBytes(apkFile));
}
if (!user.exists()) {
throw new NeneException("Packages can not be installed in non-existing users "
+ "(Trying to install into user " + user + ")");
}
if (!user.isRunning()) {
throw new NeneException("Packages can not be installed in stopped users "
+ "(Trying to install into user " + user + ")");
}
if (!user.isUnlocked()) {
throw new NeneException("Packages can not be installed in locked users "
+ "(Trying to install into user " + user + ")");
}
try (UndoableContext verification = setVerifyAdbInstalls(false)) {
// This is not in the try because if the install fails we don't want to await the broadcast
BlockingBroadcastReceiver broadcastReceiver =
registerPackageInstalledBroadcastReceiver(user);
try {
Collection<Package> beforePackages = TestApis.packages().installedForUser(user);
// Expected output "Success"
ShellCommand.builderForUser(user, "pm install")
.addOperand("-r") // Reinstall automatically
.addOperand("-t") // Allow test-only install
.addOperand(apkFile.getAbsolutePath())
.validate(ShellCommandUtils::startsWithSuccess)
.execute();
Package installedPackage = Poll.forValue("newly installed packages", () -> {
Set<Package> packages = new HashSet<>(
TestApis.packages().installedForUser(user));
packages.removeAll(beforePackages);
if (packages.isEmpty()) {
return null;
}
return packages.iterator().next();
}).toNotBeNull()
.timeout(Duration.ofSeconds(10))
.await();
if (installedPackage == null) {
installedPackage = waitForPackageAddedBroadcast(broadcastReceiver);
}
return installedPackage;
} catch (AdbException e) {
throw new NeneException("Could not install " + apkFile + " for user " + user, e);
} finally {
if (broadcastReceiver != null) {
broadcastReceiver.unregisterQuietly();
}
}
}
}
// TODO: Move this somewhere reusable (in utils)
private static byte[] loadBytes(File file) {
try (FileInputStream fis = new FileInputStream(file)) {
return readInputStreamFully(fis);
} catch (IOException e) {
throw new NeneException("Could not read file bytes for file " + file);
}
}
/**
* Install an APK from the given byte array to a given {@link UserReference}.
*
* <p>The user must be started.
*
* <p>If the package is already installed, this will replace it.
*
* <p>If the package is marked testOnly, it will still be installed.
*
* <p>When running as an instant app, this will return null. On other versions it will return
* the installed package.
*/
@Nullable
public Package install(UserReference user, byte[] apkFile) {
if (user == null || apkFile == null) {
throw new NullPointerException();
}
if (!user.exists()) {
throw new NeneException("Packages can not be installed in non-existing users "
+ "(Trying to install into user " + user + ")");
}
if (!user.isRunning()) {
throw new NeneException("Packages can not be installed in stopped users "
+ "(Trying to install into user " + user + ")");
}
if (!user.isUnlocked()) {
throw new NeneException("Packages can not be installed in locked users "
+ "(Trying to install into user " + user + ")");
}
try (UndoableContext verification = setVerifyAdbInstalls(false)) {
if (TestApis.packages().instrumented().isInstantApp()) {
// We should install using stdin with the byte array
try {
ShellCommand.builderForUser(user, "pm install")
.addOperand("-t") // Allow installing test apks
.addOperand("-r") // Replace existing apps
.addOption("-S", apkFile.length) // Install from stdin
.writeToStdIn(apkFile)
.validate(ShellCommandUtils::startsWithSuccess)
.execute();
} catch (AdbException e) {
throw new NeneException("Error installing from instant app", e);
}
// Arbitrary sleep because the shell command doesn't block and we can't listen for
// the broadcast (instant app)
try {
Thread.sleep(10_000);
} catch (InterruptedException e) {
throw new NeneException("Interrupted while waiting for install", e);
}
return null;
}
if (true || !Versions.meetsMinimumSdkVersionRequirement(Build.VERSION_CODES.S)) {
// We can't make use of -r when using SessionParams
return installUsingAdb(user, apkFile);
}
// This is not inside the try because if the install is unsuccessful we don't want to
// await the broadcast
BlockingBroadcastReceiver broadcastReceiver =
registerPackageInstalledBroadcastReceiver(user);
try {
PackageManager packageManager =
TestApis.context().androidContextAsUser(user).getPackageManager();
PackageInstaller packageInstaller = packageManager.getPackageInstaller();
int sessionId;
try (PermissionContext p = TestApis.permissions().withPermission(
INTERACT_ACROSS_USERS_FULL, INTERACT_ACROSS_USERS,
INSTALL_TEST_ONLY_PACKAGE, USE_SYSTEM_DATA_LOADERS)) {
PackageInstaller.SessionParams sessionParams =
new PackageInstaller.SessionParams(
MODE_FULL_INSTALL);
sessionParams.setInstallFlagAllowTest();
sessionId = packageInstaller.createSession(sessionParams);
}
PackageInstaller.Session session = packageInstaller.openSession(sessionId);
try (OutputStream out =
session.openWrite("NAME", 0, apkFile.length)) {
out.write(apkFile);
session.fsync(out);
}
try (BlockingIntentSender intentSender = BlockingIntentSender.create()) {
try (PermissionContext p =
TestApis.permissions().withPermission(
INSTALL_PACKAGES, INSTALL_TEST_ONLY_PACKAGE)) {
session.commit(intentSender.intentSender());
session.close();
Intent intent = intentSender.await();
if (intent == null) {
throw new NeneException(
"Did not receive intent from package installer session when"
+ " installing bytes on user " + user
+ ". Relevant logcat: "
+ TestApis.logcat().dump(
l -> l.contains("PackageInstaller")));
}
if (intent.getIntExtra(EXTRA_STATUS, /* defaultValue= */ STATUS_FAILURE)
!= STATUS_SUCCESS) {
throw new NeneException("Not successful while installing package. "
+ "Got status: "
+ intent.getIntExtra(
EXTRA_STATUS, /* defaultValue= */ STATUS_FAILURE)
+ " extra info: " + intent.getStringExtra(
EXTRA_STATUS_MESSAGE));
}
String installedPackageName = intent.getStringExtra(EXTRA_PACKAGE_NAME);
return TestApis.packages().find(installedPackageName);
}
}
} catch (IOException e) {
throw new NeneException("Could not install package", e);
} finally {
if (broadcastReceiver != null) {
broadcastReceiver.unregisterQuietly();
}
}
}
}
@Nullable
private Package installUsingAdb(UserReference user, byte[] apkFile) {
// This is not in the try because if the install fails we don't want to await the broadcast
BlockingBroadcastReceiver broadcastReceiver =
registerPackageInstalledBroadcastReceiver(user);
// We should install using stdin with the byte array
try {
Collection<Package> beforePackages = TestApis.packages().installedForUser(user);
ShellCommand.builderForUser(user, "pm install")
.addOperand("-t") // Allow installing test apks
.addOperand("-r") // Replace existing apps
.addOption("-S", apkFile.length) // Install from stdin
.writeToStdIn(apkFile)
.validate(ShellCommandUtils::startsWithSuccess)
.execute();
Package installedPackage = Poll.forValue("newly installed packages", () -> {
Set<Package> packages = new HashSet<>(
TestApis.packages().installedForUser(user));
packages.removeAll(beforePackages);
if (packages.isEmpty()) {
return null;
}
return packages.iterator().next();
}).toNotBeNull()
.timeout(Duration.ofSeconds(10))
.await();
if (installedPackage == null) {
installedPackage = waitForPackageAddedBroadcast(broadcastReceiver);
}
return installedPackage;
} catch (AdbException e) {
throw new NeneException("Error installing package", e);
} finally {
if (broadcastReceiver != null) {
broadcastReceiver.unregisterQuietly();
}
}
}
@Nullable
private Package waitForPackageAddedBroadcast(BlockingBroadcastReceiver broadcastReceiver) {
if (broadcastReceiver == null) {
// On Android versions prior to R we can't block on a broadcast for package installation
try {
Thread.sleep(20000);
} catch (InterruptedException e) {
Log.e(LOG_TAG, "Interrupted waiting for package installation", e);
}
return null;
}
Intent intent = broadcastReceiver.awaitForBroadcast();
if (intent == null) {
throw new NeneException(
"Did not receive ACTION_PACKAGE_ADDED broadcast after installing package.");
}
// TODO(scottjonathan): Could this be flaky? what if something is added elsewhere at
// the same time...
String installedPackageName = intent.getDataString().split(":", 2)[1];
return TestApis.packages().find(installedPackageName);
}
/**
* Install an APK stored in Android resources to the given {@link UserReference}.
*
* <p>The user must be started.
*
* <p>If the package is already installed, this will replace it.
*
* <p>If the package is marked testOnly, it will still be installed.
*/
@Experimental
public Package install(UserReference user, AndroidResource resource) {
int indexId = mInstrumentedContext.getResources().getIdentifier(
resource.mName, /* defType= */ null, /* defPackage= */ null);
try (InputStream inputStream =
mInstrumentedContext.getResources().openRawResource(indexId)) {
return install(user, readInputStreamFully(inputStream));
} catch (IOException e) {
throw new NeneException("Error reading resource " + resource, e);
}
}
/**
* Install an APK stored in Java resources to the given {@link UserReference}.
*
* <p>The user must be started.
*
* <p>If the package is already installed, this will replace it.
*
* <p>If the package is marked testOnly, it will still be installed.
*/
@Experimental
public Package install(UserReference user, JavaResource resource) {
try (InputStream inputStream =
Packages.class.getClassLoader().getResourceAsStream(resource.mName)) {
return install(user, readInputStreamFully(inputStream));
} catch (IOException e) {
throw new NeneException("Error reading java resource " + resource, e);
}
}
@Nullable
private BlockingBroadcastReceiver registerPackageInstalledBroadcastReceiver(
UserReference user) {
BlockingBroadcastReceiver broadcastReceiver = BlockingBroadcastReceiver.create(
TestApis.context().androidContextAsUser(user),
mPackageAddedIntentFilter);
if (user.equals(TestApis.users().instrumented())) {
broadcastReceiver.register();
} else if (Versions.meetsMinimumSdkVersionRequirement(Build.VERSION_CODES.Q)) {
try (PermissionContext p =
TestApis.permissions().withPermission(INTERACT_ACROSS_USERS_FULL)) {
broadcastReceiver.register();
}
} else {
return null;
}
return broadcastReceiver;
}
/**
* Set packages which will not be cleaned up by the system even if they are not installed on
* any user.
*
* <p>This will ensure they can still be resolved and re-installed without needing the APK
*/
@RequiresApi(Build.VERSION_CODES.S)
@CheckResult
public KeepUninstalledPackagesBuilder keepUninstalledPackages() {
Versions.requireMinimumVersion(Build.VERSION_CODES.S);
return new KeepUninstalledPackagesBuilder();
}
/**
* Get a reference to a package with the given {@code packageName}.
*
* <p>This does not guarantee that the package exists. Call {@link Package#exists()}
* to find if the package exists on the device, or {@link Package#installedOnUsers()}
* to find the users it is installed for.
*/
public Package find(String packageName) {
if (packageName == null) {
throw new NullPointerException();
}
return new Package(packageName);
}
/**
* Get a reference to a given {@code componentName} activity.
*
* <p>This does not guarantee that the component exists - nor that it is actually an activity.
*/
@Experimental
public ActivityReference activity(ComponentName componentName) {
if (componentName == null) {
throw new NullPointerException();
}
return new ActivityReference(
find(componentName.getPackageName()), componentName.getClassName());
}
/**
* Get a reference to a given {@code componentName}.
*
* <p>This does not guarantee that the component exists.
*/
@Experimental
public ComponentReference component(ComponentName componentName) {
if (componentName == null) {
throw new NullPointerException();
}
return new ComponentReference(
find(componentName.getPackageName()), componentName.getClassName());
}
/** Get a reference to the package being instrumented. */
@Experimental
public Package instrumented() {
return find(TestApis.context().instrumentedContext().getPackageName());
}
static AdbPackageParser.ParseResult parseDumpsys() {
try {
String dumpsysOutput = ShellCommand.builder("dumpsys package").execute();
return Packages.sParser.parse(dumpsysOutput);
} catch (AdbException | AdbParseException e) {
throw new NeneException("Error parsing package dumpsys", e);
}
}
/**
* System apps installed on the instrumented user.
*/
@Experimental
public Set<Package> systemApps() {
return systemApps(TestApis.users().instrumented());
}
/**
* System apps installed on the given user.
*/
@Experimental
public Set<Package> systemApps(UserReference user) {
return installedForUser(user).stream()
.filter(Package::hasSystemFlag)
.collect(Collectors.toSet());
}
/**
* Oem defined default dialer app.
*/
@Experimental
public Package oemDefaultDialerApp() {
String defaultDialerPackage = TestApis.context().instrumentedContext().getString(
Resources.getSystem().getIdentifier("config_defaultDialer", "string", "android"));
return TestApis.packages().find(defaultDialerPackage);
}
/**
* Oem defined default sms app.
*/
@Experimental
public Package oemDefaultSmsApp() {
String defaultSmsPackage = TestApis.context().instrumentedContext().getString(
Resources.getSystem().getIdentifier("config_defaultSms", "string", "android"));
return TestApis.packages().find(defaultSmsPackage);
}
@Experimental
public UndoableContext setVerifyAdbInstalls(boolean verify) {
boolean originalVerifyAdbInstalls = getVerifyAdbInstalls();
if (originalVerifyAdbInstalls == verify) {
return UndoableContext.EMPTY;
}
TestApis.settings().global().putInt(PACKAGE_VERIFIER_INCLUDE_ADB, verify ? 1 : 0);
return new UndoableContext(() -> {
setVerifyAdbInstalls(originalVerifyAdbInstalls);
});
}
@Experimental
public boolean getVerifyAdbInstalls() {
return TestApis.settings().global().getInt(PACKAGE_VERIFIER_INCLUDE_ADB, 1) == 1;
}
/**
* Get the Launcher package.
*/
@Experimental
public Package launcher() {
return find(TestApis.ui().device().getLauncherPackageName());
}
/**
* Finds the browser assigned to handle browsing intents by default for selected user.
*
* @return the package for the default browser if there is one, null otherwise.
*/
@SuppressWarnings("NewApi")
@Experimental
public Package defaultBrowserForUser(UserReference user) {
ResolveInfo resolvedActivity;
List<ResolveInfo> possibleActivities;
Intent toResolve = new Intent(ACTION_VIEW, Uri.parse("http://"));
PackageManager pm = TestApis.context()
.instrumentationContextAsUser(user)
.getPackageManager();
if (Versions.meetsMinimumSdkVersionRequirement(Versions.T)) {
possibleActivities = pm.queryIntentActivities(toResolve,
PackageManager.ResolveInfoFlags.of(MATCH_ALL));
resolvedActivity = pm.resolveActivity(toResolve,
PackageManager.ResolveInfoFlags.of(MATCH_DEFAULT_ONLY));
} else {
possibleActivities = pm.queryIntentActivities(toResolve, MATCH_ALL);
resolvedActivity = pm.resolveActivity(toResolve, MATCH_DEFAULT_ONLY);
}
Set<String> possibleBrowserPackageName = possibleActivities.stream()
.map(Packages::extractPackageName)
.filter(Objects::nonNull)
.collect(Collectors.toSet());
String resolvedBrowserPackageName = extractPackageName(resolvedActivity);
if (resolvedBrowserPackageName == null
|| !possibleBrowserPackageName.contains(resolvedBrowserPackageName)) {
return null;
}
return find(resolvedBrowserPackageName);
}
private static String extractPackageName(@Nullable ResolveInfo nullableInfo) {
return Optional.ofNullable(nullableInfo)
.map(resolveInfo -> resolveInfo.activityInfo)
.map(activityInfo -> activityInfo.packageName)
.orElse(null);
}
/** See {@link PackageManager#queryIntentActivities(Intent, int)}.
*
* <p> Returns a list of {@link ResolveInfo} wrapped in {@link ResolveInfoWrapper}.*/
@Experimental
public List<ResolveInfoWrapper> queryIntentActivities(Intent intent, int flags) {
return TestApis.context().instrumentedContext().getPackageManager()
.queryIntentActivities(intent, flags)
.stream().map(r -> new ResolveInfoWrapper(r.activityInfo, r.match))
.collect(Collectors.toList());
}
}