/*
* Copyright 2017-present Facebook, Inc.
*
* 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.facebook.buck.android;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import static org.junit.Assume.assumeTrue;
import com.android.common.SdkConstants;
import com.android.ddmlib.InstallException;
import com.facebook.buck.android.agent.util.AgentUtil;
import com.facebook.buck.android.exopackage.ExopackageDevice;
import com.facebook.buck.android.exopackage.ExopackageInstaller;
import com.facebook.buck.android.exopackage.PackageInfo;
import com.facebook.buck.io.ProjectFilesystem;
import com.facebook.buck.model.BuildTargetFactory;
import com.facebook.buck.rules.ExopackageInfo;
import com.facebook.buck.rules.FakeBuildRule;
import com.facebook.buck.rules.PathSourcePath;
import com.facebook.buck.rules.SourcePath;
import com.facebook.buck.rules.SourcePathResolver;
import com.facebook.buck.step.ExecutionContext;
import com.facebook.buck.step.TestExecutionContext;
import com.facebook.buck.testutil.MoreAsserts;
import com.facebook.buck.testutil.integration.TemporaryPaths;
import com.facebook.buck.util.environment.Platform;
import com.facebook.buck.util.sha1.Sha1HashCode;
import com.facebook.buck.zip.ZipScrubberStep;
import com.google.common.base.Charsets;
import com.google.common.base.Joiner;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSortedMap;
import com.google.common.hash.Hashing;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.NavigableMap;
import java.util.Optional;
import java.util.Set;
import java.util.TreeMap;
import java.util.TreeSet;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
public class ExopackageInstallerIntegrationTest {
private static final boolean DEBUG = false;
private static final String FAKE_PACKAGE_NAME = "buck.exotest.fake";
private static final Path INSTALL_ROOT =
ExopackageInstaller.EXOPACKAGE_INSTALL_ROOT.resolve(FAKE_PACKAGE_NAME);
@Rule public final TemporaryPaths tmpFolder = new TemporaryPaths();
private final Path apkPath = Paths.get("fake.apk");
private final Path manifestPath = Paths.get("AndroidManifest.xml");
private final Path dexDirectory = Paths.get("dex-dir");
private final Path nativeDirectory = Paths.get("native-dir");
private final Path resourcesDirectory = Paths.get("res-dir");
private final Path dexManifest = Paths.get("dex.manifest");
private final Path nativeManifest = Paths.get("native.manifest");
private final Path agentPath = Paths.get("agent.apk");
private final Path apkDevicePath = Paths.get("/data/app/Fake.apk");
private ExoState currentBuildState;
private ProjectFilesystem filesystem;
private ExecutionContext executionContext;
private TestExopackageDevice device;
private String apkVersionCode;
@Before
public void setUp() throws Exception {
assumeTrue(Platform.detect() != Platform.WINDOWS);
filesystem = new ProjectFilesystem(tmpFolder.getRoot());
executionContext = TestExecutionContext.newInstance();
currentBuildState = null;
filesystem.mkdirs(dexDirectory);
filesystem.mkdirs(nativeDirectory);
filesystem.mkdirs(resourcesDirectory);
device = new TestExopackageDevice();
apkVersionCode = "1";
device.abi = SdkConstants.ABI_ARMEABI_V7A;
}
@Test
public void testExoJavaInstall() throws Exception {
currentBuildState =
new ExoState(
"apk-content\n",
createFakeManifest("manifest-content\n"),
ImmutableList.of("secondary-dex0\n", "secondary-dex1\n"),
ImmutableSortedMap.of(),
ImmutableList.of());
checkExoInstall(1, 2, 0, 0);
}
@Test
public void testExoNativeInstall() throws Exception {
device.abi = SdkConstants.ABI_ARMEABI_V7A;
currentBuildState =
new ExoState(
"apk-content\n",
createFakeManifest("manifest-content\n"),
ImmutableList.of(),
ImmutableSortedMap.of(
"libs/" + SdkConstants.ABI_INTEL_ATOM + "/libone.so", "x86-libone\n",
"libs/" + SdkConstants.ABI_INTEL_ATOM + "/libtwo.so", "x86-libtwo\n",
"libs/" + SdkConstants.ABI_ARMEABI_V7A + "/libone.so", "armv7-libone\n",
"libs/" + SdkConstants.ABI_ARMEABI_V7A + "/libtwo.so", "armv7-libtwo\n"),
ImmutableList.of());
checkExoInstall(1, 0, 2, 0);
// This should be checked already, but do it explicitly here too to make it clear
// that we actually verify that the correct architecture libs are installed.
assertTrue(
device.deviceState.containsKey(
INSTALL_ROOT.resolve("native-libs/armeabi-v7a/metadata.txt").toString()));
assertFalse(
device.deviceState.containsKey(
INSTALL_ROOT.resolve("native-libs/x86/metadata.txt").toString()));
}
@Test
public void testExoResourcesInstall() throws Exception {
device.abi = SdkConstants.ABI_ARMEABI_V7A;
currentBuildState =
new ExoState(
"apk-content\n",
createFakeManifest("manifest-content\n"),
ImmutableList.of(),
ImmutableSortedMap.of(),
ImmutableList.of("exo-resources.apk\n", "exo-assets0\n", "exo-assets1\n"));
checkExoInstall(1, 0, 0, 3);
}
@Test
public void testExoNativeX86Install() throws Exception {
device.abi = SdkConstants.ABI_INTEL_ATOM;
currentBuildState =
new ExoState(
"apk-content\n",
createFakeManifest("manifest-content\n"),
ImmutableList.of(),
ImmutableSortedMap.of(
"libs/" + SdkConstants.ABI_INTEL_ATOM + "/libone.so", "x86-libone\n",
"libs/" + SdkConstants.ABI_INTEL_ATOM + "/libtwo.so", "x86-libtwo\n",
"libs/" + SdkConstants.ABI_ARMEABI_V7A + "/libone.so", "armv7-libone\n",
"libs/" + SdkConstants.ABI_ARMEABI_V7A + "/libtwo.so", "armv7-libtwo\n"),
ImmutableList.of());
checkExoInstall(1, 0, 2, 0);
// This should be checked already, but do it explicitly here too to make it clear
// that we actually verify that the correct architecture libs are installed.
assertFalse(
device.deviceState.containsKey(
INSTALL_ROOT.resolve("native-libs/armeabi-v7a/metadata.txt").toString()));
assertTrue(
device.deviceState.containsKey(
INSTALL_ROOT.resolve("native-libs/x86/metadata.txt").toString()));
}
@Test
public void testExoFullInstall() throws Exception {
device.abi = SdkConstants.ABI_ARMEABI_V7A;
setDefaultFullBuildState();
checkExoInstall(1, 2, 2, 3);
}
@Test
public void testExoNoopReinstall() throws Exception {
device.abi = SdkConstants.ABI_ARMEABI_V7A;
setDefaultFullBuildState();
checkExoInstall(1, 2, 2, 3);
checkExoInstall(0, 0, 0, 0);
}
private void setDefaultFullBuildState() {
currentBuildState =
new ExoState(
"apk-content\n",
createFakeManifest("manifest-content\n"),
ImmutableList.of("secondary-dex0\n", "secondary-dex1\n"),
ImmutableSortedMap.of(
"libs/" + SdkConstants.ABI_INTEL_ATOM + "/libone.so", "x86-libone\n",
"libs/" + SdkConstants.ABI_INTEL_ATOM + "/libtwo.so", "x86-libtwo\n",
"libs/" + SdkConstants.ABI_ARMEABI_V7A + "/libone.so", "armv7-libone\n",
"libs/" + SdkConstants.ABI_ARMEABI_V7A + "/libtwo.so", "armv7-libtwo\n"),
ImmutableList.of("exo-resources.apk\n", "exo-assets0\n", "exo-assets1\n"));
}
@Test
public void testExoReinstallWithApkChange() throws Exception {
device.abi = SdkConstants.ABI_ARMEABI_V7A;
setDefaultFullBuildState();
checkExoInstall(1, 2, 2, 3);
currentBuildState =
new ExoState(
"new-apk-content\n",
currentBuildState.manifestContent,
currentBuildState.secondaryDexesContents,
currentBuildState.nativeLibsContents,
currentBuildState.resourcesContents);
checkExoInstall(1, 0, 0, 0);
}
@Test
public void testExoReinstallWithJavaChange() throws Exception {
device.abi = SdkConstants.ABI_ARMEABI_V7A;
setDefaultFullBuildState();
checkExoInstall(1, 2, 2, 3);
currentBuildState =
new ExoState(
currentBuildState.apkContent,
currentBuildState.manifestContent,
ImmutableList.of("secondary-dex0\n", "new-secondary-dex1\n"),
currentBuildState.nativeLibsContents,
currentBuildState.resourcesContents);
checkExoInstall(0, 1, 0, 0);
}
@Test
public void testExoReinstallWithNativeChange() throws Exception {
device.abi = SdkConstants.ABI_ARMEABI_V7A;
setDefaultFullBuildState();
checkExoInstall(1, 2, 2, 3);
currentBuildState =
new ExoState(
currentBuildState.apkContent,
currentBuildState.manifestContent,
currentBuildState.secondaryDexesContents,
ImmutableSortedMap.of(
"libs/" + SdkConstants.ABI_INTEL_ATOM + "/libone.so", "x86-libone\n",
"libs/" + SdkConstants.ABI_INTEL_ATOM + "/libtwo.so", "new-x86-libtwo\n",
"libs/" + SdkConstants.ABI_ARMEABI_V7A + "/libone.so", "armv7-libone\n",
"libs/" + SdkConstants.ABI_ARMEABI_V7A + "/libtwo.so", "new-armv7-libtwo\n"),
currentBuildState.resourcesContents);
checkExoInstall(0, 0, 1, 0);
}
@Test
public void testExoReinstallWithResourcesChange() throws Exception {
device.abi = SdkConstants.ABI_ARMEABI_V7A;
setDefaultFullBuildState();
checkExoInstall(1, 2, 2, 3);
currentBuildState =
new ExoState(
currentBuildState.apkContent,
currentBuildState.manifestContent,
currentBuildState.secondaryDexesContents,
currentBuildState.nativeLibsContents,
ImmutableList.of("exo-resources.apk\n", "new-exo-assets0\n", "exo-assets1\n"));
checkExoInstall(0, 0, 0, 1);
}
@Test
public void testExoReinstallWithAddedDex() throws Exception {
device.abi = SdkConstants.ABI_ARMEABI_V7A;
setDefaultFullBuildState();
checkExoInstall(1, 2, 2, 3);
currentBuildState =
new ExoState(
currentBuildState.apkContent,
currentBuildState.manifestContent,
ImmutableList.of("secondary-dex0\n", "secondary-dex1\n", "secondary-dex2\n"),
currentBuildState.nativeLibsContents,
currentBuildState.resourcesContents);
checkExoInstall(0, 1, 0, 0);
}
@Test
public void testExoReinstallWithRemovedDex() throws Exception {
device.abi = SdkConstants.ABI_ARMEABI_V7A;
setDefaultFullBuildState();
checkExoInstall(1, 2, 2, 3);
currentBuildState =
new ExoState(
currentBuildState.apkContent,
currentBuildState.manifestContent,
ImmutableList.of("secondary-dex0\n"),
currentBuildState.nativeLibsContents,
currentBuildState.resourcesContents);
checkExoInstall(0, 0, 0, 0);
}
@Test
public void testExoReinstallWithAddedLib() throws Exception {
device.abi = SdkConstants.ABI_ARMEABI_V7A;
setDefaultFullBuildState();
checkExoInstall(1, 2, 2, 3);
currentBuildState =
new ExoState(
currentBuildState.apkContent,
currentBuildState.manifestContent,
currentBuildState.secondaryDexesContents,
ImmutableSortedMap.<String, String>naturalOrder()
.put("libs/" + SdkConstants.ABI_INTEL_ATOM + "/libone.so", "x86-libone\n")
.put("libs/" + SdkConstants.ABI_INTEL_ATOM + "/libtwo.so", "x86-libtwo\n")
.put("libs/" + SdkConstants.ABI_INTEL_ATOM + "/libthree.so", "x86-libthree\n")
.put("libs/" + SdkConstants.ABI_ARMEABI_V7A + "/libone.so", "armv7-libone\n")
.put("libs/" + SdkConstants.ABI_ARMEABI_V7A + "/libtwo.so", "armv7-libtwo\n")
.put("libs/" + SdkConstants.ABI_ARMEABI_V7A + "/libthree.so", "armv7-libthree\n")
.build(),
currentBuildState.resourcesContents);
checkExoInstall(0, 0, 1, 0);
}
@Test
public void testExoReinstallWithRemovedLib() throws Exception {
device.abi = SdkConstants.ABI_ARMEABI_V7A;
setDefaultFullBuildState();
checkExoInstall(1, 2, 2, 3);
currentBuildState =
new ExoState(
currentBuildState.apkContent,
currentBuildState.manifestContent,
currentBuildState.secondaryDexesContents,
ImmutableSortedMap.of(
"libs/" + SdkConstants.ABI_INTEL_ATOM + "/libone.so", "x86-libone\n",
"libs/" + SdkConstants.ABI_ARMEABI_V7A + "/libone.so", "armv7-libone\n"),
currentBuildState.resourcesContents);
checkExoInstall(0, 0, 0, 0);
}
@Test
public void testExoReinstallWithRenamedLib() throws Exception {
device.abi = SdkConstants.ABI_ARMEABI_V7A;
setDefaultFullBuildState();
checkExoInstall(1, 2, 2, 3);
currentBuildState =
new ExoState(
currentBuildState.apkContent,
currentBuildState.manifestContent,
currentBuildState.secondaryDexesContents,
ImmutableSortedMap.of(
"libs/" + SdkConstants.ABI_INTEL_ATOM + "/libone.so", "x86-libone\n",
"libs/" + SdkConstants.ABI_INTEL_ATOM + "/libtwo-new.so", "x86-libtwo\n",
"libs/" + SdkConstants.ABI_ARMEABI_V7A + "/libone.so", "armv7-libone\n",
"libs/" + SdkConstants.ABI_ARMEABI_V7A + "/libtwo-new.so", "armv7-libtwo\n"),
currentBuildState.resourcesContents);
// TODO(cjhopman): fix exo install when library is renamed but content remains the same.
// checkExoInstall(0, 0, 1, 0);
}
@Test
public void testExoReinstallWithAssetsAdded() throws Exception {
device.abi = SdkConstants.ABI_ARMEABI_V7A;
setDefaultFullBuildState();
checkExoInstall(1, 2, 2, 3);
currentBuildState =
new ExoState(
currentBuildState.apkContent,
currentBuildState.manifestContent,
currentBuildState.secondaryDexesContents,
currentBuildState.nativeLibsContents,
ImmutableList.of(
"exo-resources.apk\n", "exo-assets0\n", "exo-assets1\n", "exo-assets2\n"));
checkExoInstall(0, 0, 0, 1);
}
@Test
public void testExoReinstallWithAssetsRemoved() throws Exception {
device.abi = SdkConstants.ABI_ARMEABI_V7A;
setDefaultFullBuildState();
checkExoInstall(1, 2, 2, 3);
currentBuildState =
new ExoState(
currentBuildState.apkContent,
currentBuildState.manifestContent,
currentBuildState.secondaryDexesContents,
currentBuildState.nativeLibsContents,
ImmutableList.of("exo-resources.apk\n", "exo-assets0\n"));
checkExoInstall(0, 0, 0, 0);
}
/**
* This simulates the state of a real device enough that we can verify that exo installation
* happens correctly.
*/
private class TestExopackageDevice implements ExopackageDevice {
public String abi;
// Persistent "device" state.
private NavigableMap<String, String> deviceState;
private Set<Path> directories;
private Optional<PackageInfo> deviceAgentPackageInfo;
private Optional<PackageInfo> fakePackageInfo;
private String packageSignature;
// Per install state.
private int allowedInstalledApks;
private int allowedInstalledDexes;
private int allowedInstalledLibs;
private int allowedInstalledResources;
TestExopackageDevice() {
deviceState = new TreeMap<>();
directories = new HashSet<>();
deviceAgentPackageInfo = Optional.empty();
fakePackageInfo = Optional.empty();
allowedInstalledApks = 0;
allowedInstalledDexes = 0;
allowedInstalledLibs = 0;
allowedInstalledResources = 0;
}
@Override
public boolean installApkOnDevice(File apk, boolean installViaSd, boolean quiet) {
assertTrue(apk.isAbsolute());
if (apk.equals(filesystem.resolve(agentPath).toFile())) {
deviceAgentPackageInfo =
Optional.of(
new PackageInfo(
"/data/app/Agent.apk", "/data/data/whatever", AgentUtil.AGENT_VERSION_CODE));
return true;
} else if (apk.equals(filesystem.resolve(apkPath).toFile())) {
fakePackageInfo =
Optional.of(
new PackageInfo(
apkDevicePath.toString(), "/data/data/whatever_else", apkVersionCode));
try {
deviceState.put(apkDevicePath.toString(), filesystem.computeSha1(apkPath).toString());
packageSignature = AgentUtil.getJarSignature(apk.toString());
} catch (IOException e) {
throw new RuntimeException(e);
}
allowedInstalledApks--;
assertTrue(allowedInstalledApks >= 0);
return true;
}
throw new UnsupportedOperationException("apk path=" + apk);
}
@Override
public void stopPackage(String packageName) throws Exception {
// noop
}
@Override
public Optional<PackageInfo> getPackageInfo(String packageName) throws Exception {
if (packageName.equals(AgentUtil.AGENT_PACKAGE_NAME)) {
return deviceAgentPackageInfo;
} else if (packageName.equals(FAKE_PACKAGE_NAME)) {
return fakePackageInfo;
}
throw new UnsupportedOperationException("Tried to get package info " + packageName);
}
@Override
public void uninstallPackage(String packageName) throws InstallException {
throw new UnsupportedOperationException();
}
@Override
public String getSignature(String packagePath) throws Exception {
assertTrue(deviceState.containsKey(packagePath));
return packageSignature;
}
@Override
public String listDir(String dirPath) throws Exception {
Set<String> res = new TreeSet<>();
for (String s : deviceState.subMap(dirPath, false, dirPath + "\u00FF", false).keySet()) {
s = s.substring(dirPath.length() + 1);
if (s.contains("/")) {
res.add(s.substring(0, s.indexOf("/")));
} else {
res.add(s);
}
}
String output = Joiner.on("\n").join(res) + "\n";
debug("ls " + dirPath + "\n" + output);
return output;
}
@Override
public void rmFiles(String dirPath, Iterable<String> filesToDelete) throws Exception {
debug("rmfiles dir=" + dirPath + " files=" + ImmutableList.copyOf(filesToDelete));
for (String s : filesToDelete) {
deviceState.remove(dirPath + "/" + s);
}
}
@Override
public AutoCloseable createForward() throws Exception {
// TODO(cjhopman): track correct forwarding usage
return () -> {};
}
@Override
public void installFile(Path targetDevicePath, Path source) throws Exception {
// TODO(cjhopman): verify port and agentCommand
assertTrue(targetDevicePath.isAbsolute());
assertTrue(source.isAbsolute());
assertTrue(
String.format(
"Exopackage should only install files to the install root (%s, %s)",
INSTALL_ROOT, targetDevicePath),
targetDevicePath.startsWith(INSTALL_ROOT));
MoreAsserts.assertContainsOne(directories, targetDevicePath.getParent());
debug("installing " + targetDevicePath);
deviceState.put(targetDevicePath.toString(), filesystem.readFileIfItExists(source).get());
targetDevicePath = INSTALL_ROOT.relativize(targetDevicePath);
if (targetDevicePath.startsWith(ExopackageInstaller.SECONDARY_DEX_DIR)) {
if (!targetDevicePath.getFileName().equals(Paths.get("metadata.txt"))) {
allowedInstalledDexes--;
assertTrue(allowedInstalledDexes >= 0);
}
} else if (targetDevicePath.startsWith(ExopackageInstaller.NATIVE_LIBS_DIR)) {
if (!targetDevicePath.getFileName().equals(Paths.get("metadata.txt"))) {
allowedInstalledLibs--;
assertTrue(allowedInstalledLibs >= 0);
}
} else if (targetDevicePath.startsWith(ExopackageInstaller.RESOURCES_DIR)) {
if (!targetDevicePath.getFileName().equals(Paths.get("metadata.txt"))) {
allowedInstalledResources--;
assertTrue(allowedInstalledResources >= 0);
}
} else {
fail("Unrecognized target path (" + targetDevicePath + ")");
}
}
@Override
public void mkDirP(String dir) throws Exception {
Path dirPath = Paths.get(dir);
while (dirPath != null) {
directories.add(dirPath);
dirPath = dirPath.getParent();
}
}
@Override
public String getProperty(String name) throws Exception {
switch (name) {
case "ro.build.version.sdk":
return "20";
}
throw new UnsupportedOperationException("Tried to get prop " + name);
}
@Override
public List<String> getDeviceAbis() throws Exception {
return ImmutableList.of(abi);
}
public void setAllowedInstallCounts(
int expectedApksInstalled,
int expectedDexesInstalled,
int expectedLibsInstalled,
int expectedResourcesInstalled) {
this.allowedInstalledApks = expectedApksInstalled;
this.allowedInstalledDexes = expectedDexesInstalled;
this.allowedInstalledLibs = expectedLibsInstalled;
this.allowedInstalledResources = expectedResourcesInstalled;
}
}
private void debug(String msg) {
if (DEBUG) {
System.out.println("DBG: " + msg);
}
}
private class FakeApkRule extends FakeBuildRule implements HasInstallableApk {
private ApkInfo apkInfo;
public FakeApkRule(SourcePathResolver resolver, ApkInfo apkInfo) {
super(BuildTargetFactory.newInstance("//fake-apk-rule:apk"), filesystem, resolver);
this.apkInfo = apkInfo;
}
@Override
public ApkInfo getApkInfo() {
return apkInfo;
}
}
private class FakeAdbInterface implements ExopackageInstaller.AdbInterface {
@Override
public boolean adbCall(String description, AdbCallable func, boolean quiet)
throws InterruptedException {
try {
return func.apply(device);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
private void writeFile(Path p, String c) {
try {
debug("Writing: " + p);
if (p.getParent() != null) {
filesystem.mkdirs(p.getParent());
}
filesystem.writeContentsToPath(c, p);
} catch (IOException e) {
throw new RuntimeException("Failed to write: " + p, e);
}
}
class ExpectedStateBuilder {
Map<String, String> expectedState = new TreeMap<>();
void addApk(Path devicePath, Path hostPath) throws IOException {
expectedState.put(devicePath.toString(), filesystem.computeSha1(hostPath).toString());
}
void addExoFile(String devicePath, String content) {
expectedState.put(INSTALL_ROOT.resolve(devicePath).toString(), content);
}
}
private void checkExoInstall(
int expectedApksInstalled,
int expectedDexesInstalled,
int expectedLibsInstalled,
int expectedResourcesInstalled)
throws Exception {
SourcePathResolver pathResolver = new SourcePathResolver(null);
ExpectedStateBuilder builder = new ExpectedStateBuilder();
writeFakeApk(currentBuildState.apkContent);
writeFile(manifestPath, currentBuildState.manifestContent);
builder.addApk(apkDevicePath, apkPath);
SourcePath apkSourcePath = new PathSourcePath(filesystem, apkPath);
SourcePath manifestSourcePath = new PathSourcePath(filesystem, manifestPath);
Optional<ExopackageInfo.DexInfo> dexInfo = Optional.empty();
ImmutableList<String> dexesContents = currentBuildState.secondaryDexesContents;
if (!dexesContents.isEmpty()) {
filesystem.deleteRecursivelyIfExists(dexDirectory);
String dexMetadata = "";
String prefix = "";
for (int i = 0; i < dexesContents.size(); i++) {
String filename = "secondary-" + i + ".dex.jar";
String dexContent = dexesContents.get(i);
writeFile(dexDirectory.resolve(filename), dexContent);
Sha1HashCode dexHash = filesystem.computeSha1(dexDirectory.resolve(filename));
dexMetadata += prefix + filename + " " + dexHash;
prefix = "\n";
builder.addExoFile("secondary-dex/secondary-" + dexHash + ".dex.jar", dexContent);
}
writeFile(dexManifest, dexMetadata);
dexInfo = Optional.of(ExopackageInfo.DexInfo.of(dexManifest, dexDirectory));
builder.addExoFile("secondary-dex/metadata.txt", dexMetadata);
}
Optional<ExopackageInfo.NativeLibsInfo> nativeLibsInfo = Optional.empty();
ImmutableSortedMap<String, String> libsContents = currentBuildState.nativeLibsContents;
if (!libsContents.isEmpty()) {
filesystem.deleteRecursivelyIfExists(nativeDirectory);
String expectedMetadata = "";
String prefix = "";
for (String k : libsContents.keySet()) {
Path libPath = nativeDirectory.resolve(k);
writeFile(libPath, libsContents.get(k));
if (k.startsWith("libs/" + device.abi)) {
Sha1HashCode libHash = filesystem.computeSha1(libPath);
builder.addExoFile(
"native-libs/" + device.abi + "/native-" + libHash + ".so", libsContents.get(k));
expectedMetadata +=
prefix
+ k.substring(k.lastIndexOf("/") + 1, k.length() - 3)
+ " native-"
+ libHash
+ ".so";
prefix = "\n";
}
}
CopyNativeLibraries.createMetadataStep(filesystem, nativeManifest, nativeDirectory)
.execute(executionContext);
nativeLibsInfo =
Optional.of(ExopackageInfo.NativeLibsInfo.of(nativeManifest, nativeDirectory));
builder.addExoFile("native-libs/" + device.abi + "/metadata.txt", expectedMetadata);
}
Optional<ExopackageInfo.ResourcesInfo> resourcesInfo = Optional.empty();
if (!currentBuildState.resourcesContents.isEmpty()) {
ExopackageInfo.ResourcesInfo.Builder resourcesInfoBuilder =
ExopackageInfo.ResourcesInfo.builder();
int n = 0;
Iterator<String> resourcesContents = currentBuildState.resourcesContents.iterator();
String expectedMetadata = "";
String prefix = "";
while (resourcesContents.hasNext()) {
Path resourcePath = resourcesDirectory.resolve("resources-" + n++ + ".apk");
String content = resourcesContents.next();
writeFile(resourcePath, content);
resourcesInfoBuilder.addResourcesPaths(new PathSourcePath(filesystem, resourcePath));
Sha1HashCode resourceHash = filesystem.computeSha1(resourcePath);
expectedMetadata += prefix + "resources " + resourceHash;
prefix = "\n";
builder.addExoFile("resources/" + resourceHash + ".apk", content);
}
resourcesInfo = Optional.of(resourcesInfoBuilder.build());
builder.addExoFile("resources/metadata.txt", expectedMetadata);
}
ApkInfo apkInfo =
ApkInfo.builder()
.setApkPath(apkSourcePath)
.setManifestPath(manifestSourcePath)
.setExopackageInfo(
ExopackageInfo.builder()
.setDexInfo(dexInfo)
.setNativeLibsInfo(nativeLibsInfo)
.setResourcesInfo(resourcesInfo)
.build())
.build();
device.setAllowedInstallCounts(
expectedApksInstalled,
expectedDexesInstalled,
expectedLibsInstalled,
expectedResourcesInstalled);
try {
assertTrue(
new ExopackageInstaller(
pathResolver,
executionContext,
new FakeAdbInterface(),
new FakeApkRule(pathResolver, apkInfo))
.install(true));
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
assertEquals(builder.expectedState, device.deviceState);
assertEquals("apk should be installed but wasn't", 0, device.allowedInstalledApks);
assertEquals("fewer dexes installed than expected", 0, device.allowedInstalledDexes);
assertEquals("fewer libs installed than expected", 0, device.allowedInstalledLibs);
assertEquals("fewer resources installed than expected", 0, device.allowedInstalledResources);
}
private void writeFakeApk(String apkContent) throws IOException {
String hash = Hashing.sha1().hashString(apkContent, Charsets.US_ASCII).toString();
try (ZipOutputStream zf =
new ZipOutputStream(new FileOutputStream(filesystem.resolve(apkPath).toFile()))) {
ZipEntry signature = new ZipEntry("META-INF/SIG.SF");
zf.putNextEntry(signature);
String data = "SHA1-Digest-Manifest: " + hash + "\n";
zf.write(data.getBytes(Charsets.US_ASCII), 0, data.length());
zf.closeEntry();
ZipEntry content = new ZipEntry("content");
zf.putNextEntry(content);
zf.write(apkContent.getBytes(Charsets.US_ASCII), 0, apkContent.length());
zf.closeEntry();
}
try {
ZipScrubberStep.of(filesystem.resolve(apkPath)).execute(executionContext);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
private String createFakeManifest(String manifestContent) {
return "<?xml version='1.0' encoding='utf-8'?>\n"
+ "<manifest\n"
+ " xmlns:android='http://schemas.android.com/apk/res/android'\n"
+ " package='"
+ FAKE_PACKAGE_NAME
+ "'\n"
+ " >\n"
+ "\n"
+ " <application\n"
+ " >\n"
+ " <meta-data>"
+ manifestContent
+ "</meta-data>\n"
+ " </application>\n"
+ "\n"
+ "</manifest>";
}
private class ExoState {
private final String apkContent;
private final String manifestContent;
private final ImmutableList<String> secondaryDexesContents;
private final ImmutableSortedMap<String, String> nativeLibsContents;
private final ImmutableList<String> resourcesContents;
public ExoState(
String apkContent,
String manifestContent,
ImmutableList<String> secondaryDexesContents,
ImmutableSortedMap<String, String> nativeLibsContents,
ImmutableList<String> resourcesContents) {
this.apkContent = apkContent;
this.manifestContent = manifestContent;
this.secondaryDexesContents = secondaryDexesContents;
this.nativeLibsContents = nativeLibsContents;
this.resourcesContents = resourcesContents;
}
}
}