blob: 2c477c897b301dd6b3abbec8a543f7e4e23e4047 [file] [log] [blame]
/*
* Copyright (C) 2020 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.server.om;
import static com.android.server.om.OverlayManagerServiceImpl.OperationFailedException;
import static org.junit.Assert.assertEquals;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import android.annotation.NonNull;
import android.content.om.OverlayInfo;
import android.content.om.OverlayInfo.State;
import android.content.om.OverlayableInfo;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageInfo;
import android.text.TextUtils;
import android.util.ArrayMap;
import android.util.ArraySet;
import android.util.Pair;
import androidx.annotation.Nullable;
import com.android.internal.content.om.OverlayConfig;
import org.junit.Assert;
import org.junit.Before;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.stream.Collectors;
/** Base class for creating {@link OverlayManagerServiceImplTests} tests. */
class OverlayManagerServiceImplTestsBase {
private OverlayManagerServiceImpl mImpl;
private FakeDeviceState mState;
private FakePackageManagerHelper mPackageManager;
private FakeIdmapDaemon mIdmapDaemon;
private OverlayConfig mOverlayConfig;
private String mConfigSignaturePackageName;
@Before
public void setUp() {
mState = new FakeDeviceState();
mPackageManager = new FakePackageManagerHelper(mState);
mIdmapDaemon = new FakeIdmapDaemon(mState);
mOverlayConfig = mock(OverlayConfig.class);
when(mOverlayConfig.getPriority(any())).thenReturn(OverlayConfig.DEFAULT_PRIORITY);
when(mOverlayConfig.isEnabled(any())).thenReturn(false);
when(mOverlayConfig.isMutable(any())).thenReturn(true);
reinitializeImpl();
}
void reinitializeImpl() {
mImpl = new OverlayManagerServiceImpl(mPackageManager,
new IdmapManager(mIdmapDaemon, mPackageManager),
new OverlayManagerSettings(),
mOverlayConfig,
new String[0]);
}
OverlayManagerServiceImpl getImpl() {
return mImpl;
}
FakeIdmapDaemon getIdmapd() {
return mIdmapDaemon;
}
FakeDeviceState getState() {
return mState;
}
void setConfigSignaturePackageName(String packageName) {
mConfigSignaturePackageName = packageName;
}
void assertState(@State int expected, final String overlayPackageName, int userId) {
final OverlayInfo info = mImpl.getOverlayInfo(overlayPackageName, userId);
if (info == null) {
throw new IllegalStateException("package not installed");
}
final String msg = String.format("expected %s but was %s:",
OverlayInfo.stateToString(expected), OverlayInfo.stateToString(info.state));
assertEquals(msg, expected, info.state);
}
void assertOverlayInfoForTarget(final String targetPackageName, int userId,
OverlayInfo... overlayInfos) {
final List<OverlayInfo> expected =
mImpl.getOverlayInfosForTarget(targetPackageName, userId);
final List<OverlayInfo> actual = Arrays.asList(overlayInfos);
assertEquals(expected, actual);
}
FakeDeviceState.PackageBuilder app(String packageName) {
return new FakeDeviceState.PackageBuilder(packageName, null /* targetPackageName */,
null /* targetOverlayableName */, "data");
}
FakeDeviceState.PackageBuilder target(String packageName) {
return new FakeDeviceState.PackageBuilder(packageName, null /* targetPackageName */,
null /* targetOverlayableName */, "");
}
FakeDeviceState.PackageBuilder overlay(String packageName, String targetPackageName) {
return overlay(packageName, targetPackageName, null /* targetOverlayableName */);
}
FakeDeviceState.PackageBuilder overlay(String packageName, String targetPackageName,
String targetOverlayableName) {
return new FakeDeviceState.PackageBuilder(packageName, targetPackageName,
targetOverlayableName, "");
}
void addPackage(FakeDeviceState.PackageBuilder pkg, int userId) {
mState.add(pkg, userId);
}
void configureSystemOverlay(String packageName, boolean mutable, boolean enabled,
int priority) {
when(mOverlayConfig.getPriority(packageName)).thenReturn(priority);
when(mOverlayConfig.isEnabled(packageName)).thenReturn(enabled);
when(mOverlayConfig.isMutable(packageName)).thenReturn(mutable);
}
/**
* Adds the package to the device.
*
* This corresponds to when the OMS receives the
* {@link android.content.Intent#ACTION_PACKAGE_ADDED} broadcast.
*
* @throws IllegalStateException if the package is currently installed
*/
void installNewPackage(FakeDeviceState.PackageBuilder pkg, int userId)
throws OperationFailedException {
if (mState.select(pkg.packageName, userId) != null) {
throw new IllegalStateException("package " + pkg.packageName + " already installed");
}
mState.add(pkg, userId);
if (pkg.targetPackage == null) {
mImpl.onTargetPackageAdded(pkg.packageName, userId);
} else {
mImpl.onOverlayPackageAdded(pkg.packageName, userId);
}
}
/**
* Begins upgrading the package.
*
* This corresponds to when the OMS receives the
* {@link android.content.Intent#ACTION_PACKAGE_REMOVED} broadcast with the
* {@link android.content.Intent#EXTRA_REPLACING} extra and then receives the
* {@link android.content.Intent#ACTION_PACKAGE_ADDED} broadcast with the
* {@link android.content.Intent#EXTRA_REPLACING} extra.
*
* @return the two Optional<PackageAndUser> objects from starting and finishing the upgrade
*
* @throws IllegalStateException if the package is not currently installed
*/
Pair<Optional<PackageAndUser>, Optional<PackageAndUser>> upgradePackage(
FakeDeviceState.PackageBuilder pkg, int userId) throws OperationFailedException {
final FakeDeviceState.Package replacedPackage = mState.select(pkg.packageName, userId);
if (replacedPackage == null) {
throw new IllegalStateException("package " + pkg.packageName + " not installed");
}
Optional<PackageAndUser> opt1 = Optional.empty();
if (replacedPackage.targetPackageName != null) {
opt1 = mImpl.onOverlayPackageReplacing(pkg.packageName, userId);
}
mState.add(pkg, userId);
Optional<PackageAndUser> opt2;
if (pkg.targetPackage == null) {
opt2 = mImpl.onTargetPackageReplaced(pkg.packageName, userId);
} else {
opt2 = mImpl.onOverlayPackageReplaced(pkg.packageName, userId);
}
return Pair.create(opt1, opt2);
}
/**
* Removes the package from the device.
*
* This corresponds to when the OMS receives the
* {@link android.content.Intent#ACTION_PACKAGE_REMOVED} broadcast.
*
* @throws IllegalStateException if the package is not currently installed
*/
void uninstallPackage(String packageName, int userId) throws OperationFailedException {
final FakeDeviceState.Package pkg = mState.select(packageName, userId);
if (pkg == null) {
throw new IllegalStateException("package " + packageName+ " not installed");
}
mState.remove(pkg.packageName);
if (pkg.targetPackageName == null) {
mImpl.onTargetPackageRemoved(pkg.packageName, userId);
} else {
mImpl.onOverlayPackageRemoved(pkg.packageName, userId);
}
}
/** Represents the state of packages installed on a fake device. */
static class FakeDeviceState {
private ArrayMap<String, Package> mPackages = new ArrayMap<>();
void add(PackageBuilder pkgBuilder, int userId) {
final Package pkg = pkgBuilder.build();
final Package previousPkg = select(pkg.packageName, userId);
mPackages.put(pkg.packageName, pkg);
pkg.installedUserIds.add(userId);
if (previousPkg != null) {
pkg.installedUserIds.addAll(previousPkg.installedUserIds);
}
}
void remove(String packageName) {
mPackages.remove(packageName);
}
void uninstall(String packageName, int userId) {
final Package pkg = mPackages.get(packageName);
if (pkg != null) {
pkg.installedUserIds.remove(userId);
}
}
List<Package> select(int userId) {
return mPackages.values().stream().filter(p -> p.installedUserIds.contains(userId))
.collect(Collectors.toList());
}
Package select(String packageName, int userId) {
final Package pkg = mPackages.get(packageName);
return pkg != null && pkg.installedUserIds.contains(userId) ? pkg : null;
}
private Package selectFromPath(String path) {
return mPackages.values().stream()
.filter(p -> p.apkPath.equals(path)).findFirst().orElse(null);
}
static final class PackageBuilder {
private String packageName;
private String targetPackage;
private String certificate = "[default]";
private String partition;
private int version = 0;
private ArrayList<String> overlayableNames = new ArrayList<>();
private String targetOverlayableName;
private PackageBuilder(String packageName, String targetPackage,
String targetOverlayableName, String partition) {
this.packageName = packageName;
this.targetPackage = targetPackage;
this.targetOverlayableName = targetOverlayableName;
this.partition = partition;
}
PackageBuilder setCertificate(String certificate) {
this.certificate = certificate;
return this;
}
PackageBuilder addOverlayable(String overlayableName) {
overlayableNames.add(overlayableName);
return this;
}
PackageBuilder setVersion(int version) {
this.version = version;
return this;
}
Package build() {
String path = "";
if (TextUtils.isEmpty(partition)) {
if (targetPackage == null) {
path = "/system/app";
} else {
path = "/vendor/overlay";
}
} else {
String type = targetPackage == null ? "app" : "overlay";
path = String.format("%s/%s", partition, type);
}
final String apkPath = String.format("%s/%s/base.apk", path, packageName);
final Package newPackage = new Package(packageName, targetPackage,
targetOverlayableName, version, apkPath, certificate);
newPackage.overlayableNames.addAll(overlayableNames);
return newPackage;
}
}
static final class Package {
final String packageName;
final String targetPackageName;
final String targetOverlayableName;
final int versionCode;
final String apkPath;
final String certificate;
final ArrayList<String> overlayableNames = new ArrayList<>();
private final ArraySet<Integer> installedUserIds = new ArraySet<>();
private Package(String packageName, String targetPackageName,
String targetOverlayableName, int versionCode, String apkPath,
String certificate) {
this.packageName = packageName;
this.targetPackageName = targetPackageName;
this.targetOverlayableName = targetOverlayableName;
this.versionCode = versionCode;
this.apkPath = apkPath;
this.certificate = certificate;
}
}
}
final class FakePackageManagerHelper implements PackageManagerHelper {
private final FakeDeviceState mState;
private FakePackageManagerHelper(FakeDeviceState state) {
mState = state;
}
@Override
public PackageInfo getPackageInfo(@NonNull String packageName, int userId) {
final FakeDeviceState.Package pkg = mState.select(packageName, userId);
if (pkg == null) {
return null;
}
final ApplicationInfo ai = new ApplicationInfo();
ai.sourceDir = pkg.apkPath;
PackageInfo pi = new PackageInfo();
pi.applicationInfo = ai;
pi.packageName = pkg.packageName;
pi.overlayTarget = pkg.targetPackageName;
pi.targetOverlayableName = pkg.targetOverlayableName;
pi.overlayCategory = "Fake-category-" + pkg.targetPackageName;
return pi;
}
@Override
public boolean signaturesMatching(@NonNull String packageName1,
@NonNull String packageName2, int userId) {
final FakeDeviceState.Package pkg1 = mState.select(packageName1, userId);
final FakeDeviceState.Package pkg2 = mState.select(packageName2, userId);
return pkg1 != null && pkg2 != null && pkg1.certificate.equals(pkg2.certificate);
}
@Override
public List<PackageInfo> getOverlayPackages(int userId) {
return mState.select(userId).stream()
.filter(p -> p.targetPackageName != null)
.map(p -> getPackageInfo(p.packageName, userId))
.collect(Collectors.toList());
}
@Override
public @NonNull String getConfigSignaturePackage() {
return mConfigSignaturePackageName;
}
@Nullable
@Override
public OverlayableInfo getOverlayableForTarget(@NonNull String packageName,
@NonNull String targetOverlayableName, int userId) {
final FakeDeviceState.Package pkg = mState.select(packageName, userId);
if (pkg == null || !pkg.overlayableNames.contains(targetOverlayableName)) {
return null;
}
return new OverlayableInfo(targetOverlayableName, null /* actor */);
}
@Nullable
@Override
public String[] getPackagesForUid(int uid) {
throw new UnsupportedOperationException();
}
@NonNull
@Override
public Map<String, Map<String, String>> getNamedActors() {
return Collections.emptyMap();
}
@Override
public boolean doesTargetDefineOverlayable(String targetPackageName, int userId) {
final FakeDeviceState.Package pkg = mState.select(targetPackageName, userId);
return pkg != null && pkg.overlayableNames.contains(targetPackageName);
}
@Override
public void enforcePermission(String permission, String message) throws SecurityException {
throw new UnsupportedOperationException();
}
}
static class FakeIdmapDaemon extends IdmapDaemon {
private final FakeDeviceState mState;
private final ArrayMap<String, IdmapHeader> mIdmapFiles = new ArrayMap<>();
FakeIdmapDaemon(FakeDeviceState state) {
this.mState = state;
}
private int getCrc(@NonNull final String path) {
final FakeDeviceState.Package pkg = mState.selectFromPath(path);
Assert.assertNotNull(pkg);
return pkg.versionCode;
}
@Override
String createIdmap(String targetPath, String overlayPath, int policies, boolean enforce,
int userId) {
mIdmapFiles.put(overlayPath, new IdmapHeader(getCrc(targetPath),
getCrc(overlayPath), targetPath, policies, enforce));
return overlayPath;
}
@Override
boolean removeIdmap(String overlayPath, int userId) {
return mIdmapFiles.remove(overlayPath) != null;
}
@Override
boolean verifyIdmap(String targetPath, String overlayPath, int policies, boolean enforce,
int userId) {
final IdmapHeader idmap = mIdmapFiles.get(overlayPath);
if (idmap == null) {
return false;
}
return idmap.isUpToDate(getCrc(targetPath), getCrc(overlayPath), targetPath, policies,
enforce);
}
@Override
boolean idmapExists(String overlayPath, int userId) {
return mIdmapFiles.containsKey(overlayPath);
}
IdmapHeader getIdmap(String overlayPath) {
return mIdmapFiles.get(overlayPath);
}
static class IdmapHeader {
private final int targetCrc;
private final int overlayCrc;
final String targetPath;
final int policies;
final boolean enforceOverlayable;
private IdmapHeader(int targetCrc, int overlayCrc, String targetPath, int policies,
boolean enforceOverlayable) {
this.targetCrc = targetCrc;
this.overlayCrc = overlayCrc;
this.targetPath = targetPath;
this.policies = policies;
this.enforceOverlayable = enforceOverlayable;
}
private boolean isUpToDate(int expectedTargetCrc, int expectedOverlayCrc,
String expectedTargetPath, int expectedPolicies,
boolean expectedEnforceOverlayable) {
return expectedTargetCrc == targetCrc && expectedOverlayCrc == overlayCrc
&& expectedTargetPath.equals(targetPath) && expectedPolicies == policies
&& expectedEnforceOverlayable == enforceOverlayable;
}
}
}
}