blob: c61e6d58a45405555535fc82744f67a1c151d1ae [file] [log] [blame]
/*
* Copyright (C) 2010 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 vogar.android;
import java.io.File;
import java.io.FilenameFilter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.TimeoutException;
import vogar.Classpath;
import vogar.HostFileCache;
import vogar.Log;
import vogar.Md5Cache;
import vogar.ModeId;
import vogar.commands.Command;
import vogar.commands.Mkdir;
import vogar.util.Strings;
/**
* Android SDK commands such as adb, aapt and dx.
*/
public class AndroidSdk {
// $BOOTCLASSPATH defined by system/core/rootdir/init.rc
public static final String[] BOOTCLASSPATH = new String[] { "core-libart",
"conscrypt",
"okhttp",
"core-junit",
"bouncycastle",
"ext",
"framework",
"telephony-common",
"mms-common",
"framework",
"android.policy",
"services",
"apache-xml"};
public static final String[] HOST_BOOTCLASSPATH = new String[] {
"core-libart-hostdex",
"conscrypt-hostdex",
"okhttp-hostdex",
"bouncycastle-hostdex",
};
private final Log log;
private final Mkdir mkdir;
private final File[] compilationClasspath;
public final DeviceFilesystem deviceFilesystem;
private Md5Cache dexCache;
private Md5Cache pushCache;
public static Collection<File> defaultExpectations() {
File[] files = new File("libcore/expectations").listFiles(new FilenameFilter() {
// ignore obviously temporary files
public boolean accept(File dir, String name) {
return !name.endsWith("~") && !name.startsWith(".");
}
});
return (files != null) ? Arrays.asList(files) : Collections.<File>emptyList();
}
public AndroidSdk(Log log, Mkdir mkdir, ModeId modeId) {
this.log = log;
this.mkdir = mkdir;
this.deviceFilesystem = new DeviceFilesystem(log, "adb", "shell");
List<String> path = new Command(log, "which", "adb").execute();
if (path.isEmpty()) {
throw new RuntimeException("adb not found");
}
File adb = new File(path.get(0)).getAbsoluteFile();
String parentFileName = adb.getParentFile().getName();
/*
* We probably get aapt/adb/dx from either a copy of the Android SDK or a copy
* of the Android source code. In either case, all three tools are in the same
* directory as each other.
*
* Android SDK >= v9 (gingerbread):
* <sdk>/platform-tools/aapt
* <sdk>/platform-tools/adb
* <sdk>/platform-tools/dx
* <sdk>/platforms/android-?/android.jar
*
* Android build tree (target):
* <source>/out/host/linux-x86/bin/aapt
* <source>/out/host/linux-x86/bin/adb
* <source>/out/host/linux-x86/bin/dx
* <source>/out/target/common/obj/JAVA_LIBRARIES/core-libart_intermediates/classes.jar
*/
if ("platform-tools".equals(parentFileName)) {
File sdkRoot = adb.getParentFile().getParentFile();
File newestPlatform = getNewestPlatform(sdkRoot);
log.verbose("using android platform: " + newestPlatform);
compilationClasspath = new File[] { new File(newestPlatform, "android.jar") };
log.verbose("using android sdk: " + sdkRoot);
} else if ("bin".equals(parentFileName)) {
File sourceRoot = adb.getParentFile().getParentFile()
.getParentFile().getParentFile().getParentFile();
log.verbose("using android build tree: " + sourceRoot);
String pattern = "out/target/common/obj/JAVA_LIBRARIES/%s_intermediates/classes.jar";
if (modeId.isHost()) {
pattern = "out/host/common/obj/JAVA_LIBRARIES/%s_intermediates/classes.jar";
}
String[] jarNames = modeId.getJarNames();
compilationClasspath = new File[jarNames.length];
for (int i = 0; i < jarNames.length; i++) {
String jar = jarNames[i];
compilationClasspath[i] = new File(sourceRoot, String.format(pattern, jar));
}
} else {
throw new RuntimeException("Couldn't derive Android home from " + adb);
}
}
/**
* Returns the platform directory that has the highest API version. API
* platform directories are named like "android-9" or "android-11".
*/
private File getNewestPlatform(File sdkRoot) {
File newestPlatform = null;
int newestPlatformVersion = 0;
for (File platform : new File(sdkRoot, "platforms").listFiles()) {
try {
int version = Integer.parseInt(platform.getName().substring("android-".length()));
if (version > newestPlatformVersion) {
newestPlatform = platform;
newestPlatformVersion = version;
}
} catch (NumberFormatException ignore) {
// Ignore non-numeric preview versions like android-Honeycomb
}
}
return newestPlatform;
}
public static Collection<File> defaultSourcePath() {
return filterNonExistentPathsFrom("libcore/support/src/test/java",
"external/mockwebserver/src/main/java/");
}
private static Collection<File> filterNonExistentPathsFrom(String... paths) {
ArrayList<File> result = new ArrayList<File>();
String buildRoot = System.getenv("ANDROID_BUILD_TOP");
for (String path : paths) {
File file = new File(buildRoot, path);
if (file.exists()) {
result.add(file);
}
}
return result;
}
public File[] getCompilationClasspath() {
return compilationClasspath;
}
public void setCaches(HostFileCache hostFileCache, DeviceFileCache deviceCache) {
this.dexCache = new Md5Cache(log, "dex", hostFileCache);
this.pushCache = new Md5Cache(log, "pushed", deviceCache);
}
/**
* Converts all the .class files on 'classpath' into a dex file written to 'output'.
*/
public void dex(File output, Classpath classpath) {
mkdir.mkdirs(output.getParentFile());
String key = dexCache.makeKey(classpath);
if (key != null) {
boolean cacheHit = dexCache.getFromCache(output, key);
if (cacheHit) {
log.verbose("dex cache hit for " + classpath);
return;
}
}
/*
* We pass --core-library so that we can write tests in the
* same package they're testing, even when that's a core
* library package. If you're actually just using this tool to
* execute arbitrary code, this has the unfortunate
* side-effect of preventing "dx" from protecting you from
* yourself.
*
* Memory options pulled from build/core/definitions.mk to
* handle large dx input when building dex for APK.
*/
new Command.Builder(log)
.args("dx")
.args("-JXms16M")
.args("-JXmx1536M")
.args("--dex")
.args("--output=" + output)
.args("--core-library")
.args((Object[]) Strings.objectsToStrings(classpath.getElements())).execute();
dexCache.insert(key, output);
}
public void packageApk(File apk, File manifest) {
List<String> aapt = new ArrayList<String>(Arrays.asList("aapt",
"package",
"-F", apk.getPath(),
"-M", manifest.getPath(),
"-I", "prebuilts/sdk/current/android.jar"));
new Command(log, aapt).execute();
}
public void addToApk(File apk, File dex) {
new Command(log, "aapt", "add", "-k", apk.getPath(), dex.getPath()).execute();
}
public void mv(File source, File destination) {
new Command(log, "adb", "shell", "mv", source.getPath(), destination.getPath()).execute();
}
public void rm(File name) {
new Command(log, "adb", "shell", "rm", "-r", name.getPath()).execute();
}
public void cp(File source, File destination) {
// adb doesn't support "cp" command directly
new Command(log, "adb", "shell", "cat", source.getPath(), ">", destination.getPath())
.execute();
}
public void pull(File remote, File local) {
new Command(log, "adb", "pull", remote.getPath(), local.getPath()).execute();
}
public void push(File local, File remote) {
Command fallback = new Command(log, "adb", "push", local.getPath(), remote.getPath());
deviceFilesystem.mkdirs(remote.getParentFile());
// don't yet cache directories (only used by jtreg tests)
if (pushCache != null && local.isFile()) {
String key = pushCache.makeKey(local);
boolean cacheHit = pushCache.getFromCache(remote, key);
if (cacheHit) {
log.verbose("device cache hit for " + local);
return;
}
fallback.execute();
pushCache.insert(key, remote);
} else {
fallback.execute();
}
}
public void install(File apk) {
new Command(log, "adb", "install", "-r", apk.getPath()).execute();
}
public void uninstall(String packageName) {
new Command(log, "adb", "uninstall", packageName).execute();
}
public void forwardTcp(int port) {
new Command(log, "adb", "forward", "tcp:" + port, "tcp:" + port).execute();
}
public void remount() {
new Command(log, "adb", "remount").execute();
}
public void waitForDevice() {
new Command(log, "adb", "wait-for-device").execute();
}
/**
* Make sure the directory exists.
*/
public void ensureDirectory(File path) {
String pathArgument = path.getPath() + "/";
if (pathArgument.equals("/sdcard/")) {
// /sdcard is a mount point. If it exists but is empty we do
// not want to use it. So we wait until it is not empty.
waitForNonEmptyDirectory(pathArgument, 5 * 60);
} else {
Command command = new Command(log, "adb", "shell", "ls", pathArgument);
List<String> output = command.execute();
// TODO: We should avoid checking for the error message, and instead have
// the Command class understand a non-zero exit code from an adb shell command.
if (!output.isEmpty()
&& output.get(0).equals(pathArgument + ": No such file or directory")) {
throw new RuntimeException("'" + pathArgument + "' does not exist on device");
}
// Otherwise the directory exists.
}
}
private void waitForNonEmptyDirectory(String pathArgument, int timeoutSeconds) {
final int millisPerSecond = 1000;
final long start = System.currentTimeMillis();
final long deadline = start + (millisPerSecond * timeoutSeconds);
while (true) {
final int remainingSeconds =
(int) ((deadline - System.currentTimeMillis()) / millisPerSecond);
Command command = new Command(log, "adb", "shell", "ls", pathArgument);
List<String> output;
try {
output = command.executeWithTimeout(remainingSeconds);
} catch (TimeoutException e) {
throw new RuntimeException("Timed out after " + timeoutSeconds
+ " seconds waiting for " + pathArgument, e);
}
try {
Thread.sleep(millisPerSecond);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
// We just want any output.
if (!output.isEmpty()) {
return;
}
log.warn("Waiting on " + pathArgument + " to be mounted ");
}
}
}