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