1/* 2 * Copyright (C) 2010 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17package vogar.android; 18 19import java.io.File; 20import java.io.FilenameFilter; 21import java.util.ArrayList; 22import java.util.Arrays; 23import java.util.Collection; 24import java.util.Collections; 25import java.util.List; 26import java.util.concurrent.TimeoutException; 27import vogar.Classpath; 28import vogar.HostFileCache; 29import vogar.Log; 30import vogar.Md5Cache; 31import vogar.ModeId; 32import vogar.commands.Command; 33import vogar.commands.Mkdir; 34import vogar.util.Strings; 35 36/** 37 * Android SDK commands such as adb, aapt and dx. 38 */ 39public class AndroidSdk { 40 41 // $BOOTCLASSPATH defined by system/core/rootdir/init.rc 42 public static final String[] BOOTCLASSPATH = new String[] { "core-libart", 43 "conscrypt", 44 "okhttp", 45 "core-junit", 46 "bouncycastle", 47 "ext", 48 "framework", 49 "telephony-common", 50 "mms-common", 51 "framework", 52 "android.policy", 53 "services", 54 "apache-xml"}; 55 56 57 public static final String[] HOST_BOOTCLASSPATH = new String[] { 58 "core-libart-hostdex", 59 "conscrypt-hostdex", 60 "okhttp-hostdex", 61 "bouncycastle-hostdex", 62 "apache-xml-hostdex", 63 }; 64 65 private final Log log; 66 private final Mkdir mkdir; 67 private final File[] compilationClasspath; 68 public final DeviceFilesystem deviceFilesystem; 69 70 private Md5Cache dexCache; 71 private Md5Cache pushCache; 72 73 public static Collection<File> defaultExpectations() { 74 File[] files = new File("libcore/expectations").listFiles(new FilenameFilter() { 75 // ignore obviously temporary files 76 public boolean accept(File dir, String name) { 77 return !name.endsWith("~") && !name.startsWith("."); 78 } 79 }); 80 return (files != null) ? Arrays.asList(files) : Collections.<File>emptyList(); 81 } 82 83 public AndroidSdk(Log log, Mkdir mkdir, ModeId modeId) { 84 this.log = log; 85 this.mkdir = mkdir; 86 this.deviceFilesystem = new DeviceFilesystem(log, "adb", "shell"); 87 88 List<String> path = new Command(log, "which", "adb").execute(); 89 if (path.isEmpty()) { 90 throw new RuntimeException("adb not found"); 91 } 92 File adb = new File(path.get(0)).getAbsoluteFile(); 93 String parentFileName = adb.getParentFile().getName(); 94 95 /* 96 * We probably get aapt/adb/dx from either a copy of the Android SDK or a copy 97 * of the Android source code. In either case, all three tools are in the same 98 * directory as each other. 99 * 100 * Android SDK >= v9 (gingerbread): 101 * <sdk>/platform-tools/aapt 102 * <sdk>/platform-tools/adb 103 * <sdk>/platform-tools/dx 104 * <sdk>/platforms/android-?/android.jar 105 * 106 * Android build tree (target): 107 * <source>/out/host/linux-x86/bin/aapt 108 * <source>/out/host/linux-x86/bin/adb 109 * <source>/out/host/linux-x86/bin/dx 110 * <source>/out/target/common/obj/JAVA_LIBRARIES/core-libart_intermediates/classes.jar 111 */ 112 113 if ("platform-tools".equals(parentFileName)) { 114 File sdkRoot = adb.getParentFile().getParentFile(); 115 File newestPlatform = getNewestPlatform(sdkRoot); 116 log.verbose("using android platform: " + newestPlatform); 117 compilationClasspath = new File[] { new File(newestPlatform, "android.jar") }; 118 log.verbose("using android sdk: " + sdkRoot); 119 } else if ("bin".equals(parentFileName)) { 120 File sourceRoot = adb.getParentFile().getParentFile() 121 .getParentFile().getParentFile().getParentFile(); 122 log.verbose("using android build tree: " + sourceRoot); 123 124 String pattern = "out/target/common/obj/JAVA_LIBRARIES/%s_intermediates/classes.jar"; 125 if (modeId.isHost()) { 126 pattern = "out/host/common/obj/JAVA_LIBRARIES/%s_intermediates/classes.jar"; 127 } 128 129 String[] jarNames = modeId.getJarNames(); 130 compilationClasspath = new File[jarNames.length]; 131 for (int i = 0; i < jarNames.length; i++) { 132 String jar = jarNames[i]; 133 compilationClasspath[i] = new File(sourceRoot, String.format(pattern, jar)); 134 } 135 } else { 136 throw new RuntimeException("Couldn't derive Android home from " + adb); 137 } 138 } 139 140 /** 141 * Returns the platform directory that has the highest API version. API 142 * platform directories are named like "android-9" or "android-11". 143 */ 144 private File getNewestPlatform(File sdkRoot) { 145 File newestPlatform = null; 146 int newestPlatformVersion = 0; 147 for (File platform : new File(sdkRoot, "platforms").listFiles()) { 148 try { 149 int version = Integer.parseInt(platform.getName().substring("android-".length())); 150 if (version > newestPlatformVersion) { 151 newestPlatform = platform; 152 newestPlatformVersion = version; 153 } 154 } catch (NumberFormatException ignore) { 155 // Ignore non-numeric preview versions like android-Honeycomb 156 } 157 } 158 return newestPlatform; 159 } 160 161 public static Collection<File> defaultSourcePath() { 162 return filterNonExistentPathsFrom("libcore/support/src/test/java", 163 "external/mockwebserver/src/main/java/"); 164 } 165 166 private static Collection<File> filterNonExistentPathsFrom(String... paths) { 167 ArrayList<File> result = new ArrayList<File>(); 168 String buildRoot = System.getenv("ANDROID_BUILD_TOP"); 169 for (String path : paths) { 170 File file = new File(buildRoot, path); 171 if (file.exists()) { 172 result.add(file); 173 } 174 } 175 return result; 176 } 177 178 public File[] getCompilationClasspath() { 179 return compilationClasspath; 180 } 181 182 public void setCaches(HostFileCache hostFileCache, DeviceFileCache deviceCache) { 183 this.dexCache = new Md5Cache(log, "dex", hostFileCache); 184 this.pushCache = new Md5Cache(log, "pushed", deviceCache); 185 } 186 187 /** 188 * Converts all the .class files on 'classpath' into a dex file written to 'output'. 189 */ 190 public void dex(File output, Classpath classpath) { 191 mkdir.mkdirs(output.getParentFile()); 192 193 String key = dexCache.makeKey(classpath); 194 if (key != null) { 195 boolean cacheHit = dexCache.getFromCache(output, key); 196 if (cacheHit) { 197 log.verbose("dex cache hit for " + classpath); 198 return; 199 } 200 } 201 202 /* 203 * We pass --core-library so that we can write tests in the 204 * same package they're testing, even when that's a core 205 * library package. If you're actually just using this tool to 206 * execute arbitrary code, this has the unfortunate 207 * side-effect of preventing "dx" from protecting you from 208 * yourself. 209 * 210 * Memory options pulled from build/core/definitions.mk to 211 * handle large dx input when building dex for APK. 212 */ 213 new Command.Builder(log) 214 .args("dx") 215 .args("-JXms16M") 216 .args("-JXmx1536M") 217 .args("--dex") 218 .args("--output=" + output) 219 .args("--core-library") 220 .args((Object[]) Strings.objectsToStrings(classpath.getElements())).execute(); 221 dexCache.insert(key, output); 222 } 223 224 public void packageApk(File apk, File manifest) { 225 List<String> aapt = new ArrayList<String>(Arrays.asList("aapt", 226 "package", 227 "-F", apk.getPath(), 228 "-M", manifest.getPath(), 229 "-I", "prebuilts/sdk/current/android.jar")); 230 new Command(log, aapt).execute(); 231 } 232 233 public void addToApk(File apk, File dex) { 234 new Command(log, "aapt", "add", "-k", apk.getPath(), dex.getPath()).execute(); 235 } 236 237 public void mv(File source, File destination) { 238 new Command(log, "adb", "shell", "mv", source.getPath(), destination.getPath()).execute(); 239 } 240 241 public void rm(File name) { 242 new Command(log, "adb", "shell", "rm", "-r", name.getPath()).execute(); 243 } 244 245 public void cp(File source, File destination) { 246 // adb doesn't support "cp" command directly 247 new Command(log, "adb", "shell", "cat", source.getPath(), ">", destination.getPath()) 248 .execute(); 249 } 250 251 public void pull(File remote, File local) { 252 new Command(log, "adb", "pull", remote.getPath(), local.getPath()).execute(); 253 } 254 255 public void push(File local, File remote) { 256 Command fallback = new Command(log, "adb", "push", local.getPath(), remote.getPath()); 257 deviceFilesystem.mkdirs(remote.getParentFile()); 258 // don't yet cache directories (only used by jtreg tests) 259 if (pushCache != null && local.isFile()) { 260 String key = pushCache.makeKey(local); 261 boolean cacheHit = pushCache.getFromCache(remote, key); 262 if (cacheHit) { 263 log.verbose("device cache hit for " + local); 264 return; 265 } 266 fallback.execute(); 267 pushCache.insert(key, remote); 268 } else { 269 fallback.execute(); 270 } 271 } 272 273 public void install(File apk) { 274 new Command(log, "adb", "install", "-r", apk.getPath()).execute(); 275 } 276 277 public void uninstall(String packageName) { 278 new Command(log, "adb", "uninstall", packageName).execute(); 279 } 280 281 public void forwardTcp(int port) { 282 new Command(log, "adb", "forward", "tcp:" + port, "tcp:" + port).execute(); 283 } 284 285 public void remount() { 286 new Command(log, "adb", "remount").execute(); 287 } 288 289 public void waitForDevice() { 290 new Command(log, "adb", "wait-for-device").execute(); 291 } 292 293 /** 294 * Make sure the directory exists. 295 */ 296 public void ensureDirectory(File path) { 297 String pathArgument = path.getPath() + "/"; 298 if (pathArgument.equals("/sdcard/")) { 299 // /sdcard is a mount point. If it exists but is empty we do 300 // not want to use it. So we wait until it is not empty. 301 waitForNonEmptyDirectory(pathArgument, 5 * 60); 302 } else { 303 Command command = new Command(log, "adb", "shell", "ls", pathArgument); 304 List<String> output = command.execute(); 305 // TODO: We should avoid checking for the error message, and instead have 306 // the Command class understand a non-zero exit code from an adb shell command. 307 if (!output.isEmpty() 308 && output.get(0).equals(pathArgument + ": No such file or directory")) { 309 throw new RuntimeException("'" + pathArgument + "' does not exist on device"); 310 } 311 // Otherwise the directory exists. 312 } 313 } 314 315 private void waitForNonEmptyDirectory(String pathArgument, int timeoutSeconds) { 316 final int millisPerSecond = 1000; 317 final long start = System.currentTimeMillis(); 318 final long deadline = start + (millisPerSecond * timeoutSeconds); 319 320 while (true) { 321 final int remainingSeconds = 322 (int) ((deadline - System.currentTimeMillis()) / millisPerSecond); 323 Command command = new Command(log, "adb", "shell", "ls", pathArgument); 324 List<String> output; 325 try { 326 output = command.executeWithTimeout(remainingSeconds); 327 } catch (TimeoutException e) { 328 throw new RuntimeException("Timed out after " + timeoutSeconds 329 + " seconds waiting for " + pathArgument, e); 330 } 331 try { 332 Thread.sleep(millisPerSecond); 333 } catch (InterruptedException e) { 334 throw new RuntimeException(e); 335 } 336 337 // We just want any output. 338 if (!output.isEmpty()) { 339 return; 340 } 341 342 log.warn("Waiting on " + pathArgument + " to be mounted "); 343 } 344 } 345} 346