/* -*- mode: java; c-basic-offset: 2; indent-tabs-mode: nil -*- */ /* Part of the Processing project - http://processing.org Copyright (c) 2009-11 Ben Fry and Casey Reas This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License version 2 as published by the Free Software Foundation. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ package processing.mode.android; import java.io.*; import java.util.*; import org.apache.tools.ant.*; import processing.app.*; import processing.app.exec.*; import processing.core.PApplet; import processing.mode.java.JavaBuild; class AndroidBuild extends JavaBuild { static final String basePackage = "processing.android.test"; static final String sdkVersion = "7"; // private final Editor editor; private final AndroidSDK sdk; Manifest manifest; // String className; /** temporary folder safely inside a 8.3-friendly folder */ File tmpFolder; /** build.xml file for this project */ File buildFile; public AndroidBuild(final Sketch sketch, final AndroidSDK sdk) { super(sketch); this.sdk = sdk; } public File createProject(String target, File coreZipFile) throws IOException, SketchException { tmpFolder = createTempBuildFolder(sketch); // Create the 'src' folder with the preprocessed code. final File srcFolder = new File(tmpFolder, "src"); if (processing.app.Base.DEBUG) { Base.openFolder(tmpFolder); } manifest = new Manifest(sketch); // grab code from current editing window (GUI only) // prepareExport(null); // build the preproc and get to work AndroidPreprocessor preproc = new AndroidPreprocessor(sketch, getPackageName()); if (!preproc.parseSketchSize()) { throw new SketchException("Could not parse the size() command."); } sketchClassName = preprocess(srcFolder, manifest.getPackageName(), preproc); if (sketchClassName != null) { File tempManifest = new File(tmpFolder, "AndroidManifest.xml"); manifest.writeBuild(tempManifest, sketchClassName, target.equals("debug")); writeBuildProps(new File(tmpFolder, "build.properties")); buildFile = new File(tmpFolder, "build.xml"); writeBuildXML(buildFile, sketch.getName()); writeDefaultProps(new File(tmpFolder, "default.properties")); writeLocalProps(new File(tmpFolder, "local.properties")); writeRes(new File(tmpFolder, "res"), sketchClassName); final File libsFolder = mkdirs(tmpFolder, "libs"); final File assetsFolder = mkdirs(tmpFolder, "assets"); // InputStream input = PApplet.createInput(getCoreZipLocation()); // PApplet.saveStream(new File(libsFolder, "processing-core.jar"), input); Base.copyFile(coreZipFile, new File(libsFolder, "processing-core.jar")); try { // Copy any imported libraries or code folder contents to the project writeLibraries(libsFolder, assetsFolder); // Copy the data folder, if one exists, to the 'assets' folder of the // project final File sketchDataFolder = sketch.getDataFolder(); if (sketchDataFolder.exists()) { Base.copyDir(sketchDataFolder, assetsFolder); } } catch (final IOException e) { e.printStackTrace(); throw new SketchException(e.getMessage()); } } // } catch (final SketchException e) { // editor.statusError(e); // return null; // } catch (final IOException e) { // editor.statusError(e); // return null; // } return tmpFolder; } /** * The Android dex util pukes on paths containing spaces, which will happen * most of the time on Windows, since Processing sketches wind up in * "My Documents". Therefore, build android in a temp file. * http://code.google.com/p/android/issues/detail?id=4567 * * TODO: better would be to retrieve the 8.3 name for the sketch folder! * * @param sketch * @return A folder in which to build the android sketch * @throws IOException */ private File createTempBuildFolder(final Sketch sketch) throws IOException { final File tmp = File.createTempFile("android", ".pde"); if (!(tmp.delete() && tmp.mkdir())) { throw new IOException("Cannot create temp dir " + tmp + " to build android sketch"); } return tmp; } protected File createExportFolder() throws IOException { // Sketch sketch = editor.getSketch(); // Create the 'android' build folder, and move any existing version out. File androidFolder = new File(sketch.getFolder(), "android"); if (androidFolder.exists()) { // Date mod = new Date(androidFolder.lastModified()); String stamp = AndroidMode.getDateStamp(androidFolder.lastModified()); File dest = new File(sketch.getFolder(), "android." + stamp); boolean result = androidFolder.renameTo(dest); if (!result) { ProcessHelper mv; ProcessResult pr; try { System.err.println("createProject renameTo() failed, resorting to mv/move instead."); mv = new ProcessHelper("mv", androidFolder.getAbsolutePath(), dest.getAbsolutePath()); pr = mv.execute(); // } catch (IOException e) { // editor.statusError(e); // return null; // } catch (InterruptedException e) { e.printStackTrace(); return null; } if (!pr.succeeded()) { System.err.println(pr.getStderr()); Base.showWarning("Failed to rename", "Could not rename the old “android” build folder.\n" + "Please delete, close, or rename the folder\n" + androidFolder.getAbsolutePath() + "\n" + "and try again." , null); Base.openFolder(sketch.getFolder()); return null; } } } else { boolean result = androidFolder.mkdirs(); if (!result) { Base.showWarning("Folders, folders, folders", "Could not create the necessary folders to build.\n" + "Perhaps you have some file permissions to sort out?", null); return null; } } return androidFolder; } /** * @param target "debug" or "release" */ protected boolean antBuild(final String target) throws SketchException { final Project p = new Project(); String path = buildFile.getAbsolutePath().replace('\\', '/'); p.setUserProperty("ant.file", path); // deals with a problem where javac error messages weren't coming through p.setUserProperty("build.compiler", "extJavac"); // p.setUserProperty("build.compiler.emacs", "true"); // does nothing final DefaultLogger consoleLogger = new DefaultLogger(); consoleLogger.setErrorPrintStream(System.err); // consoleLogger.setOutputPrintStream(System.out); consoleLogger.setMessageOutputLevel(Project.MSG_ERR); p.addBuildListener(consoleLogger); final DefaultLogger errorLogger = new DefaultLogger(); final ByteArrayOutputStream errb = new ByteArrayOutputStream(); final PrintStream errp = new PrintStream(errb); errorLogger.setErrorPrintStream(errp); final ByteArrayOutputStream outb = new ByteArrayOutputStream(); final PrintStream outp = new PrintStream(outb); errorLogger.setOutputPrintStream(outp); errorLogger.setMessageOutputLevel(Project.MSG_INFO); p.addBuildListener(errorLogger); try { // editor.statusNotice("Building sketch for Android..."); p.fireBuildStarted(); p.init(); final ProjectHelper helper = ProjectHelper.getProjectHelper(); p.addReference("ant.projectHelper", helper); helper.parse(p, buildFile); // p.executeTarget(p.getDefaultTarget()); p.executeTarget(target); // editor.statusNotice("Finished building sketch."); return true; } catch (final BuildException e) { // Send a "build finished" event to the build listeners for this project. p.fireBuildFinished(e); // PApplet.println(new String(errb.toByteArray())); // PApplet.println(new String(outb.toByteArray())); // String errorOutput = new String(errb.toByteArray()); // String[] errorLines = // errorOutput.split(System.getProperty("line.separator")); // PApplet.println(errorLines); final String outPile = new String(outb.toByteArray()); final String[] outLines = outPile.split(System.getProperty("line.separator")); // PApplet.println(outLines); for (final String line : outLines) { final String javacPrefix = "[javac]"; final int javacIndex = line.indexOf(javacPrefix); if (javacIndex != -1) { // System.out.println("checking: " + line); // final Sketch sketch = editor.getSketch(); // String sketchPath = sketch.getFolder().getAbsolutePath(); int offset = javacIndex + javacPrefix.length() + 1; String[] pieces = PApplet.match(line.substring(offset), "^(.+):([0-9]+):\\s+(.+)$"); if (pieces != null) { // PApplet.println(pieces); String fileName = pieces[1]; // remove the path from the front of the filename fileName = fileName.substring(fileName.lastIndexOf('/') + 1); final int lineNumber = PApplet.parseInt(pieces[2]) - 1; // PApplet.println("looking for " + fileName + " line " + // lineNumber); SketchException rex = placeException(pieces[3], fileName, lineNumber); if (rex != null) { // rex.hideStackTrace(); // editor.statusError(rex); // return false; // get outta here throw rex; } } } } // editor.statusError(e); System.err.println("Problem during build:"); e.printStackTrace(); // Couldn't parse the exception, so wrap it up and chuck it // throw new SketchException(e); } return false; } // protected String getClassName() { // return className; // } String getPathForAPK(final String target) { String suffix = target.equals("release") ? "unsigned" : "debug"; String apkName = "bin/" + sketch.getName() + "-" + suffix + ".apk"; final File apkFile = new File(tmpFolder, apkName); return apkFile.getAbsolutePath(); } private void writeBuildProps(final File file) { final PrintWriter writer = PApplet.createWriter(file); writer.println("application-package=" + getPackageName()); writer.flush(); writer.close(); } private void writeBuildXML(final File file, final String projectName) { final PrintWriter writer = PApplet.createWriter(file); writer.println(""); writer.println(""); writer.println(" "); writer.println(" "); writer.println(" "); writer.println(" "); writer.println(" "); writer.println(" "); writer.println(" "); writer.println(" "); writer.println(" "); writer.println(" "); writer.println(" "); writer.println(" "); writer.println(""); writer.flush(); writer.close(); } private void writeDefaultProps(final File file) { final PrintWriter writer = PApplet.createWriter(file); writer.println("target=Google Inc.:Google APIs:" + sdkVersion); writer.flush(); writer.close(); } private void writeLocalProps(final File file) { final PrintWriter writer = PApplet.createWriter(file); final String sdkPath = sdk.getSdkFolder().getAbsolutePath(); if (Base.isWindows()) { // Windows needs backslashes escaped, or it will also accept forward // slashes in the build file. We're using the forward slashes since this // path gets concatenated with a lot of others that use forwards anyway. writer.println("sdk.dir=" + sdkPath.replace('\\', '/')); } else { writer.println("sdk.dir=" + sdkPath); } writer.flush(); writer.close(); } static final String ICON_72 = "icon-72.png"; static final String ICON_48 = "icon-48.png"; static final String ICON_36 = "icon-36.png"; private void writeRes(File resFolder, String className) throws SketchException { File layoutFolder = mkdirs(resFolder, "layout"); File layoutFile = new File(layoutFolder, "main.xml"); writeResLayoutMain(layoutFile); // write the icon files File sketchFolder = sketch.getFolder(); File localIcon36 = new File(sketchFolder, ICON_36); File localIcon48 = new File(sketchFolder, ICON_48); File localIcon72 = new File(sketchFolder, ICON_72); // File drawableFolder = new File(resFolder, "drawable"); // drawableFolder.mkdirs() File buildIcon48 = new File(resFolder, "drawable/icon.png"); File buildIcon36 = new File(resFolder, "drawable-ldpi/icon.png"); File buildIcon72 = new File(resFolder, "drawable-hdpi/icon.png"); if (!localIcon36.exists() && !localIcon48.exists() && !localIcon72.exists()) { try { // if no icons are in the sketch folder, then copy all the defaults if (buildIcon36.getParentFile().mkdirs()) { Base.copyFile(mode.getContentFile("icons/" + ICON_36), buildIcon36); } else { System.err.println("Could not create \"drawable-ldpi\" folder."); } if (buildIcon48.getParentFile().mkdirs()) { Base.copyFile(mode.getContentFile("icons/" + ICON_48), buildIcon48); } else { System.err.println("Could not create \"drawable\" folder."); } if (buildIcon72.getParentFile().mkdirs()) { Base.copyFile(mode.getContentFile("icons/" + ICON_72), buildIcon72); } else { System.err.println("Could not create \"drawable-hdpi\" folder."); } } catch (IOException e) { e.printStackTrace(); //throw new SketchException("Could not get Android icons"); } } else { // if at least one of the icons already exists, then use that across the board try { if (localIcon36.exists()) { if (new File(resFolder, "drawable-ldpi").mkdirs()) { Base.copyFile(localIcon36, buildIcon36); } } if (localIcon48.exists()) { if (new File(resFolder, "drawable").mkdirs()) { Base.copyFile(localIcon48, buildIcon48); } } if (localIcon72.exists()) { if (new File(resFolder, "drawable-hdpi").mkdirs()) { Base.copyFile(localIcon72, buildIcon72); } } } catch (IOException e) { System.err.println("Problem while copying icons."); e.printStackTrace(); } } // final File valuesFolder = mkdirs(resFolder, "values"); // final File stringsFile = new File(valuesFolder, "strings.xml"); // writeResValuesStrings(stringsFile, className); } private File mkdirs(final File parent, final String name) throws SketchException { final File result = new File(parent, name); if (!(result.exists() || result.mkdirs())) { throw new SketchException("Could not create " + result); } return result; } private void writeResLayoutMain(final File file) { final PrintWriter writer = PApplet.createWriter(file); writer.println(""); writer.println(""); writer.println(""); writer.flush(); writer.close(); } // This recommended to be a string resource so that it can be localized. // nah.. we're gonna be messing with it in the GUI anyway... // people can edit themselves if they need to // private static void writeResValuesStrings(final File file, // final String className) { // final PrintWriter writer = PApplet.createWriter(file); // writer.println(""); // writer.println(""); // writer.println(" " + className + ""); // writer.println(""); // writer.flush(); // writer.close(); // } private void writeLibraries(final File libsFolder, final File assetsFolder) throws IOException { // Copy any libraries to the 'libs' folder for (Library library : getImportedLibraries()) { File libraryFolder = new File(library.getPath()); // in the list is a File object that points the // library sketch's "library" folder final File exportSettings = new File(libraryFolder, "export.txt"); final HashMap exportTable = Base.readSettings(exportSettings); final String androidList = exportTable.get("android"); String exportList[] = null; if (androidList != null) { exportList = PApplet.splitTokens(androidList, ", "); } else { exportList = libraryFolder.list(); } for (int i = 0; i < exportList.length; i++) { exportList[i] = PApplet.trim(exportList[i]); if (exportList[i].equals("") || exportList[i].equals(".") || exportList[i].equals("..")) { continue; } final File exportFile = new File(libraryFolder, exportList[i]); if (!exportFile.exists()) { System.err.println("File " + exportList[i] + " does not exist"); } else if (exportFile.isDirectory()) { System.err.println("Ignoring sub-folder \"" + exportList[i] + "\""); } else { final String name = exportFile.getName(); final String lcname = name.toLowerCase(); if (lcname.endsWith(".zip") || lcname.endsWith(".jar")) { // As of r4 of the Android SDK, it looks like .zip files // are ignored in the libs folder, so rename to .jar final String jarName = name.substring(0, name.length() - 4) + ".jar"; Base.copyFile(exportFile, new File(libsFolder, jarName)); } else { Base.copyFile(exportFile, new File(assetsFolder, name)); } } } } // Copy files from the 'code' directory into the 'libs' folder final File codeFolder = sketch.getCodeFolder(); if (codeFolder != null && codeFolder.exists()) { for (final File item : codeFolder.listFiles()) { if (!item.isDirectory()) { final String name = item.getName(); final String lcname = name.toLowerCase(); if (lcname.endsWith(".jar") || lcname.endsWith(".zip")) { String jarName = name.substring(0, name.length() - 4) + ".jar"; Base.copyFile(item, new File(libsFolder, jarName)); } } } } } protected String getPackageName() { return manifest.getPackageName(); } public void cleanup() { // don't want to be responsible for this //rm(tempBuildFolder); tmpFolder.deleteOnExit(); } }