// -*- mode: java; c-basic-offset: 2; -*-
// Copyright 2009-2011 Google, All Rights reserved
// Copyright 2011-2016 MIT, All rights reserved
// Released under the Apache License, Version 2.0
// http://www.apache.org/licenses/LICENSE-2.0
package com.google.appinventor.buildserver;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Charsets;
import com.google.common.base.Strings;
import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
import com.google.common.io.Files;
import com.google.common.io.Resources;
import com.android.sdklib.build.ApkBuilder;
import org.codehaus.jettison.json.JSONArray;
import org.codehaus.jettison.json.JSONException;
import org.codehaus.jettison.json.JSONObject;
import org.codehaus.jettison.json.JSONTokener;
import java.awt.image.BufferedImage;
import java.io.BufferedWriter;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.PrintStream;
import java.io.Reader;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.imageio.ImageIO;
/**
* Main entry point for the YAIL compiler.
*
* <p>Supplies entry points for building Young Android projects.
*
* @author markf@google.com (Mark Friedman)
* @author lizlooney@google.com (Liz Looney)
*
* [Will 2016/9/20, Refactored {@link #writeAndroidManifest(File)} to
* accomodate the new annotations for adding <activity> and <receiver>
* elements.]
*/
public final class Compiler {
/**
* reading guide:
* Comp == Component, comp == component, COMP == COMPONENT
* Ext == External, ext == external, EXT == EXTERNAL
*/
public static int currentProgress = 10;
// Kawa and DX processes can use a lot of memory. We only launch one Kawa or DX process at a time.
private static final Object SYNC_KAWA_OR_DX = new Object();
private static final String SLASH = File.separator;
private static final String COLON = File.pathSeparator;
private static final String WEBVIEW_ACTIVITY_CLASS =
"com.google.appinventor.components.runtime.WebViewActivity";
// Copied from SdkLevel.java (which isn't in our class path so we duplicate it here)
private static final String LEVEL_GINGERBREAD_MR1 = "10";
public static final String RUNTIME_FILES_DIR = "/" + "files" + "/";
// Build info constants. Used for permissions, libraries, assets and activities.
// Must match ComponentProcessor.ARMEABI_V7A_SUFFIX
private static final String ARMEABI_V7A_SUFFIX = "-v7a";
// Must match Component.ASSET_DIRECTORY
private static final String ASSET_DIRECTORY = "component";
// Must match ComponentListGenerator.ASSETS_TARGET
private static final String ASSETS_TARGET = "assets";
// Must match ComponentListGenerator.ACTIVITIES_TARGET
private static final String ACTIVITIES_TARGET = "activities";
// Must match ComponentListGenerator.LIBRARIES_TARGET
public static final String LIBRARIES_TARGET = "libraries";
// Must match ComponentListGenerator.NATIVE_TARGET
public static final String NATIVE_TARGET = "native";
// Must match ComponentListGenerator.PERMISSIONS_TARGET
private static final String PERMISSIONS_TARGET = "permissions";
// Must match ComponentListGenerator.BROADCAST_RECEIVERS_TARGET
private static final String BROADCAST_RECEIVERS_TARGET = "broadcastReceivers";
// TODO(Will): Remove the following target once the deprecated
// @SimpleBroadcastReceiver annotation is removed. It should
// should remain for the time being because otherwise we'll break
// extensions currently using @SimpleBroadcastReceiver.
//
// Must match ComponentListGenerator.BROADCAST_RECEIVER_TARGET
private static final String BROADCAST_RECEIVER_TARGET = "broadcastReceiver";
// Native library directory names
private static final String LIBS_DIR_NAME = "libs";
private static final String ARMEABI_DIR_NAME = "armeabi";
private static final String ARMEABI_V7A_DIR_NAME = "armeabi-v7a";
private static final String EXT_COMPS_DIR_NAME = "external_comps";
private static final String DEFAULT_APP_NAME = "";
private static final String DEFAULT_ICON = RUNTIME_FILES_DIR + "ya.png";
private static final String DEFAULT_VERSION_CODE = "1";
private static final String DEFAULT_VERSION_NAME = "1.0";
private static final String DEFAULT_MIN_SDK = "4";
/*
* Resource paths to yail runtime, runtime library files and sdk tools.
* To get the real file paths, call getResource() with one of these constants.
*/
private static final String ACRA_RUNTIME =
RUNTIME_FILES_DIR + "acra-4.4.0.jar";
private static final String ANDROID_RUNTIME =
RUNTIME_FILES_DIR + "android.jar";
private static final String COMP_BUILD_INFO =
RUNTIME_FILES_DIR + "simple_components_build_info.json";
private static final String DX_JAR =
RUNTIME_FILES_DIR + "dx.jar";
private static final String KAWA_RUNTIME =
RUNTIME_FILES_DIR + "kawa.jar";
private static final String SIMPLE_ANDROID_RUNTIME_JAR =
RUNTIME_FILES_DIR + "AndroidRuntime.jar";
private static final String LINUX_AAPT_TOOL =
"/tools/linux/aapt";
private static final String LINUX_ZIPALIGN_TOOL =
"/tools/linux/zipalign";
private static final String MAC_AAPT_TOOL =
"/tools/mac/aapt";
private static final String MAC_ZIPALIGN_TOOL =
"/tools/mac/zipalign";
private static final String WINDOWS_AAPT_TOOL =
"/tools/windows/aapt";
private static final String WINDOWS_ZIPALIGN_TOOL =
"/tools/windows/zipalign";
@VisibleForTesting
static final String YAIL_RUNTIME = RUNTIME_FILES_DIR + "runtime.scm";
private final ConcurrentMap<String, Set<String>> assetsNeeded =
new ConcurrentHashMap<String, Set<String>>();
private final ConcurrentMap<String, Set<String>> activitiesNeeded =
new ConcurrentHashMap<String, Set<String>>();
private final ConcurrentMap<String, Set<String>> broadcastReceiversNeeded =
new ConcurrentHashMap<String, Set<String>>();
private final ConcurrentMap<String, Set<String>> libsNeeded =
new ConcurrentHashMap<String, Set<String>>();
private final ConcurrentMap<String, Set<String>> nativeLibsNeeded =
new ConcurrentHashMap<String, Set<String>>();
private final ConcurrentMap<String, Set<String>> permissionsNeeded =
new ConcurrentHashMap<String, Set<String>>();
private final Set<String> uniqueLibsNeeded = Sets.newHashSet();
// TODO(Will): Remove the following Set once the deprecated
// @SimpleBroadcastReceiver annotation is removed. It should
// should remain for the time being because otherwise we'll break
// extensions currently using @SimpleBroadcastReceiver.
private final ConcurrentMap<String, Set<String>> componentBroadcastReceiver =
new ConcurrentHashMap<String, Set<String>>();
/**
* Map used to hold the names and paths of resources that we've written out
* as temp files.
* Don't use this map directly. Please call getResource() with one of the
* constants above to get the (temp file) path to a resource.
*/
private static final ConcurrentMap<String, File> resources =
new ConcurrentHashMap<String, File>();
// TODO(user,lizlooney): i18n here and in lines below that call String.format(...)
private static final String COMPILATION_ERROR =
"Error: Your build failed due to an error when compiling %s.\n";
private static final String ERROR_IN_STAGE =
"Error: Your build failed due to an error in the %s stage, " +
"not because of an error in your program.\n";
private static final String ICON_ERROR =
"Error: Your build failed because %s cannot be used as the application icon.\n";
private static final String NO_USER_CODE_ERROR =
"Error: No user code exists.\n";
private final int childProcessRamMb; // Maximum ram that can be used by a child processes, in MB.
private final boolean isForCompanion;
private final Project project;
private final PrintStream out;
private final PrintStream err;
private final PrintStream userErrors;
private File libsDir; // The directory that will contain any native libraries for packaging
private String dexCacheDir;
private boolean hasSecondDex = false; // True if classes2.dex should be added to the APK
private JSONArray simpleCompsBuildInfo;
private JSONArray extCompsBuildInfo;
private Set<String> simpleCompTypes; // types needed by the project
private Set<String> extCompTypes; // types needed by the project
private static final Logger LOG = Logger.getLogger(Compiler.class.getName());
/*
* Generate the set of Android permissions needed by this project.
*/
@VisibleForTesting
void generatePermissions() {
try {
loadJsonInfo(permissionsNeeded, PERMISSIONS_TARGET);
if (project != null) { // Only do this if we have a project (testing doesn't provide one :-( ).
LOG.log(Level.INFO, "usesLocation = " + project.getUsesLocation());
if (project.getUsesLocation().equals("True")) { // Add location permissions if any WebViewer requests it
Set<String> locationPermissions = Sets.newHashSet(); // via a Property.
// See ProjectEditor.recordLocationSettings()
locationPermissions.add("android.permission.ACCESS_FINE_LOCATION");
locationPermissions.add("android.permission.ACCESS_COARSE_LOCATION");
locationPermissions.add("android.permission.ACCESS_MOCK_LOCATION");
permissionsNeeded.put("com.google.appinventor.components.runtime.WebViewer", locationPermissions);
}
}
} catch (IOException e) {
// This is fatal.
e.printStackTrace();
userErrors.print(String.format(ERROR_IN_STAGE, "Permissions"));
} catch (JSONException e) {
// This is fatal, but shouldn't actually ever happen.
e.printStackTrace();
userErrors.print(String.format(ERROR_IN_STAGE, "Permissions"));
}
int n = 0;
for (String type : permissionsNeeded.keySet()) {
n += permissionsNeeded.get(type).size();
}
System.out.println("Permissions needed, n = " + n);
}
// Just used for testing
@VisibleForTesting
Map<String,Set<String>> getPermissions() {
return permissionsNeeded;
}
// Just used for testing
@VisibleForTesting
Map<String, Set<String>> getBroadcastReceivers() {
return broadcastReceiversNeeded;
}
// Just used for testing
@VisibleForTesting
Map<String, Set<String>> getActivities() {
return activitiesNeeded;
}
/*
* Generate the set of Android libraries needed by this project.
*/
@VisibleForTesting
void generateLibNames() {
try {
loadJsonInfo(libsNeeded, LIBRARIES_TARGET);
} catch (IOException e) {
// This is fatal.
e.printStackTrace();
userErrors.print(String.format(ERROR_IN_STAGE, "Libraries"));
} catch (JSONException e) {
// This is fatal, but shouldn't actually ever happen.
e.printStackTrace();
userErrors.print(String.format(ERROR_IN_STAGE, "Libraries"));
}
int n = 0;
for (String type : libsNeeded.keySet()) {
n += libsNeeded.get(type).size();
}
System.out.println("Libraries needed, n = " + n);
}
/*
* Generate the set of conditionally included libraries needed by this project.
*/
@VisibleForTesting
void generateNativeLibNames() {
try {
loadJsonInfo(nativeLibsNeeded, NATIVE_TARGET);
} catch (IOException e) {
// This is fatal.
e.printStackTrace();
userErrors.print(String.format(ERROR_IN_STAGE, "Native Libraries"));
} catch (JSONException e) {
// This is fatal, but shouldn't actually ever happen.
e.printStackTrace();
userErrors.print(String.format(ERROR_IN_STAGE, "Native Libraries"));
}
int n = 0;
for (String type : nativeLibsNeeded.keySet()) {
n += nativeLibsNeeded.get(type).size();
}
System.out.println("Native Libraries needed, n = " + n);
}
/*
* Generate the set of conditionally included assets needed by this project.
*/
@VisibleForTesting
void generateAssets() {
try {
loadJsonInfo(assetsNeeded, ASSETS_TARGET);
} catch (IOException e) {
// This is fatal.
e.printStackTrace();
userErrors.print(String.format(ERROR_IN_STAGE, "Assets"));
} catch (JSONException e) {
// This is fatal, but shouldn't actually ever happen.
e.printStackTrace();
userErrors.print(String.format(ERROR_IN_STAGE, "Assets"));
}
int n = 0;
for (String type : assetsNeeded.keySet()) {
n += assetsNeeded.get(type).size();
}
System.out.println("Component assets needed, n = " + n);
}
/*
* Generate the set of conditionally included activities needed by this project.
*/
@VisibleForTesting
void generateActivities() {
try {
loadJsonInfo(activitiesNeeded, ACTIVITIES_TARGET);
} catch (IOException e) {
// This is fatal.
e.printStackTrace();
userErrors.print(String.format(ERROR_IN_STAGE, "Activities"));
} catch (JSONException e) {
// This is fatal, but shouldn't actually ever happen.
e.printStackTrace();
userErrors.print(String.format(ERROR_IN_STAGE, "Activities"));
}
int n = 0;
for (String type : activitiesNeeded.keySet()) {
n += activitiesNeeded.get(type).size();
}
System.out.println("Component activities needed, n = " + n);
}
/*
* Generate a set of conditionally included broadcast receivers needed by this project.
*/
@VisibleForTesting
void generateBroadcastReceivers() {
try {
loadJsonInfo(broadcastReceiversNeeded, BROADCAST_RECEIVERS_TARGET);
}
catch (IOException e) {
// This is fatal.
e.printStackTrace();
userErrors.print(String.format(ERROR_IN_STAGE, "BroadcastReceivers"));
} catch (JSONException e) {
// This is fatal, but shouldn't actually ever happen.
e.printStackTrace();
userErrors.print(String.format(ERROR_IN_STAGE, "BroadcastReceivers"));
}
}
/*
* TODO(Will): Remove this method once the deprecated @SimpleBroadcastReceiver
* annotation is removed. This should remain for the time being so
* that we don't break extensions currently using the
* @SimpleBroadcastReceiver annotation.
*/
@VisibleForTesting
void generateBroadcastReceiver() {
try {
loadJsonInfo(componentBroadcastReceiver, BROADCAST_RECEIVER_TARGET);
}
catch (IOException e) {
// This is fatal.
e.printStackTrace();
userErrors.print(String.format(ERROR_IN_STAGE, "BroadcastReceiver"));
} catch (JSONException e) {
// This is fatal, but shouldn't actually ever happen.
e.printStackTrace();
userErrors.print(String.format(ERROR_IN_STAGE, "BroadcastReceiver"));
}
}
// This patches around a bug in AAPT (and other placed in Android)
// where an ampersand in the name string breaks AAPT.
private String cleanName(String name) {
return name.replace("&", "and");
}
/*
* Creates an AndroidManifest.xml file needed for the Android application.
*/
private boolean writeAndroidManifest(File manifestFile) {
// Create AndroidManifest.xml
String mainClass = project.getMainClass();
String packageName = Signatures.getPackageName(mainClass);
String className = Signatures.getClassName(mainClass);
String projectName = project.getProjectName();
String vCode = (project.getVCode() == null) ? DEFAULT_VERSION_CODE : project.getVCode();
String vName = (project.getVName() == null) ? DEFAULT_VERSION_NAME : cleanName(project.getVName());
String aName = (project.getAName() == null) ? DEFAULT_APP_NAME : cleanName(project.getAName());
String minSDK = DEFAULT_MIN_SDK;
LOG.log(Level.INFO, "VCode: " + project.getVCode());
LOG.log(Level.INFO, "VName: " + project.getVName());
// TODO(user): Use com.google.common.xml.XmlWriter
try {
BufferedWriter out = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(manifestFile), "UTF-8"));
out.write("<?xml version=\"1.0\" encoding=\"utf-8\"?>\n");
// TODO(markf) Allow users to set versionCode and versionName attributes.
// See http://developer.android.com/guide/publishing/publishing.html for
// more info.
out.write("<manifest " +
"xmlns:android=\"http://schemas.android.com/apk/res/android\" " +
"package=\"" + packageName + "\" " +
// TODO(markf): uncomment the following line when we're ready to enable publishing to the
// Android Market.
"android:versionCode=\"" + vCode +"\" " + "android:versionName=\"" + vName + "\" " +
">\n");
// If we are building the Wireless Debugger (AppInventorDebugger) add the uses-feature tag which
// is used by the Google Play store to determine which devices the app is available for. By adding
// these lines we indicate that we use these features BUT THAT THEY ARE NOT REQUIRED so it is ok
// to make the app available on devices that lack the feature. Without these lines the Play Store
// makes a guess based on permissions and assumes that they are required features.
if (isForCompanion) {
out.write(" <uses-feature android:name=\"android.hardware.bluetooth\" android:required=\"false\" />\n");
out.write(" <uses-feature android:name=\"android.hardware.location\" android:required=\"false\" />\n");
out.write(" <uses-feature android:name=\"android.hardware.telephony\" android:required=\"false\" />\n");
out.write(" <uses-feature android:name=\"android.hardware.location.network\" android:required=\"false\" />\n");
out.write(" <uses-feature android:name=\"android.hardware.location.gps\" android:required=\"false\" />\n");
out.write(" <uses-feature android:name=\"android.hardware.microphone\" android:required=\"false\" />\n");
out.write(" <uses-feature android:name=\"android.hardware.touchscreen\" android:required=\"false\" />\n");
out.write(" <uses-feature android:name=\"android.hardware.camera\" android:required=\"false\" />\n");
out.write(" <uses-feature android:name=\"android.hardware.camera.autofocus\" android:required=\"false\" />\n");
out.write(" <uses-feature android:name=\"android.hardware.wifi\" />\n"); // We actually require wifi
}
// Firebase requires at least API 10 (Gingerbread MR1)
if (simpleCompTypes.contains("com.google.appinventor.components.runtime.FirebaseDB") && !isForCompanion) {
minSDK = LEVEL_GINGERBREAD_MR1;
}
// make permissions unique by putting them in one set
Set<String> permissions = Sets.newHashSet();
for (Set<String> compPermissions : permissionsNeeded.values()) {
permissions.addAll(compPermissions);
}
for (String permission : permissions) {
out.write(" <uses-permission android:name=\"" + permission + "\" />\n");
}
if (isForCompanion) { // This is so ACRA can do a logcat on phones older then Jelly Bean
out.write(" <uses-permission android:name=\"android.permission.READ_LOGS\" />\n");
}
// TODO(markf): Change the minSdkVersion below if we ever require an SDK beyond 1.5.
// The market will use the following to filter apps shown to devices that don't support
// the specified SDK version. We right now support building for minSDK 4.
// We might also want to allow users to specify minSdk version or targetSDK version.
out.write(" <uses-sdk android:minSdkVersion=\"" + minSDK + "\" />\n");
out.write(" <application ");
// TODO(markf): The preparing to publish doc at
// http://developer.android.com/guide/publishing/preparing.html suggests removing the
// 'debuggable=true' but I'm not sure that our users would want that while they're still
// testing their packaged apps. Maybe we should make that an option, somehow.
// TODONE(jis): Turned off debuggable. No one really uses it and it represents a security
// risk for App Inventor App end-users.
out.write("android:debuggable=\"false\" ");
if (aName.equals("")) {
out.write("android:label=\"" + projectName + "\" ");
} else {
out.write("android:label=\"" + aName + "\" ");
}
out.write("android:icon=\"@drawable/ya\" ");
if (isForCompanion) { // This is to hook into ACRA
out.write("android:name=\"com.google.appinventor.components.runtime.ReplApplication\" ");
} else {
out.write("android:name=\"com.google.appinventor.components.runtime.multidex.MultiDexApplication\" ");
}
out.write(">\n");
for (Project.SourceDescriptor source : project.getSources()) {
String formClassName = source.getQualifiedName();
// String screenName = formClassName.substring(formClassName.lastIndexOf('.') + 1);
boolean isMain = formClassName.equals(mainClass);
if (isMain) {
// The main activity of the application.
out.write(" <activity android:name=\"." + className + "\" ");
} else {
// A secondary activity of the application.
out.write(" <activity android:name=\"" + formClassName + "\" ");
}
// This line is here for NearField and NFC. It keeps the activity from
// restarting every time NDEF_DISCOVERED is signaled.
// TODO: Check that this doesn't screw up other components. Also, it might be
// better to do this programmatically when the NearField component is created, rather
// than here in the manifest.
if (simpleCompTypes.contains("com.google.appinventor.components.runtime.NearField") &&
!isForCompanion && isMain) {
out.write("android:launchMode=\"singleTask\" ");
} else if (isMain && isForCompanion) {
out.write("android:launchMode=\"singleTop\" ");
}
out.write("android:windowSoftInputMode=\"stateHidden\" ");
// The keyboard option prevents the app from stopping when a external (bluetooth)
// keyboard is attached.
out.write("android:configChanges=\"orientation|keyboardHidden|keyboard\">\n");
out.write(" <intent-filter>\n");
out.write(" <action android:name=\"android.intent.action.MAIN\" />\n");
if (isMain) {
out.write(" <category android:name=\"android.intent.category.LAUNCHER\" />\n");
}
out.write(" </intent-filter>\n");
if (simpleCompTypes.contains("com.google.appinventor.components.runtime.NearField") &&
!isForCompanion && isMain) {
// make the form respond to NDEF_DISCOVERED
// this will trigger the form's onResume method
// For now, we're handling text/plain only,but we can add more and make the Nearfield
// component check the type.
out.write(" <intent-filter>\n");
out.write(" <action android:name=\"android.nfc.action.NDEF_DISCOVERED\" />\n");
out.write(" <category android:name=\"android.intent.category.DEFAULT\" />\n");
out.write(" <data android:mimeType=\"text/plain\" />\n");
out.write(" </intent-filter>\n");
}
out.write(" </activity>\n");
}
// Collect any additional <application> subelements into a single set.
Set<Map.Entry<String, Set<String>>> subelements = Sets.newHashSet();
subelements.addAll(activitiesNeeded.entrySet());
subelements.addAll(broadcastReceiversNeeded.entrySet());
// If any component needs to register additional activities or
// broadcast receivers, insert them into the manifest here.
if (!subelements.isEmpty()) {
for (Map.Entry<String, Set<String>> componentSubElSetPair : subelements) {
Set<String> subelementSet = componentSubElSetPair.getValue();
for (String subelement : subelementSet) {
out.write(subelement);
}
}
}
// TODO(Will): Remove the following legacy code once the deprecated
// @SimpleBroadcastReceiver annotation is removed. It should
// should remain for the time being because otherwise we'll break
// extensions currently using @SimpleBroadcastReceiver.
// Collect any legacy simple broadcast receivers
Set<String> simpleBroadcastReceivers = Sets.newHashSet();
for (String componentType : componentBroadcastReceiver.keySet()) {
simpleBroadcastReceivers.addAll(componentBroadcastReceiver.get(componentType));
}
// The format for each legacy Broadcast Receiver in simpleBroadcastReceivers is
// "className,Action1,Action2,..." where the class name is mandatory, and
// actions are optional (and as many as needed).
for (String broadcastReceiver : simpleBroadcastReceivers) {
String[] brNameAndActions = broadcastReceiver.split(",");
if (brNameAndActions.length == 0) continue;
out.write(
"<receiver android:name=\"" + brNameAndActions[0] + "\" >\n");
if (brNameAndActions.length > 1){
out.write(" <intent-filter>\n");
for (int i = 1; i < brNameAndActions.length; i++) {
out.write(" <action android:name=\"" + brNameAndActions[i] + "\" />\n");
}
out.write(" </intent-filter>\n");
}
out.write("</receiver> \n");
}
out.write(" </application>\n");
out.write("</manifest>\n");
out.close();
} catch (IOException e) {
e.printStackTrace();
userErrors.print(String.format(ERROR_IN_STAGE, "manifest"));
return false;
}
return true;
}
/**
* Builds a YAIL project.
*
* @param project project to build
* @param compTypes component types used in the project
* @param out stdout stream for compiler messages
* @param err stderr stream for compiler messages
* @param userErrors stream to write user-visible error messages
* @param keystoreFilePath
* @param childProcessRam maximum RAM for child processes, in MBs.
* @return {@code true} if the compilation succeeds, {@code false} otherwise
* @throws JSONException
* @throws IOException
*/
public static boolean compile(Project project, Set<String> compTypes,
PrintStream out, PrintStream err, PrintStream userErrors,
boolean isForCompanion, String keystoreFilePath,
int childProcessRam, String dexCacheDir) throws IOException, JSONException {
long start = System.currentTimeMillis();
// Create a new compiler instance for the compilation
Compiler compiler = new Compiler(project, compTypes, out, err, userErrors, isForCompanion,
childProcessRam, dexCacheDir);
compiler.generateAssets();
compiler.generateActivities();
compiler.generateBroadcastReceivers();
compiler.generateLibNames();
compiler.generateNativeLibNames();
compiler.generatePermissions();
// TODO(Will): Remove the following call once the deprecated
// @SimpleBroadcastReceiver annotation is removed. It should
// should remain for the time being because otherwise we'll break
// extensions currently using @SimpleBroadcastReceiver.
compiler.generateBroadcastReceiver();
// Create build directory.
File buildDir = createDir(project.getBuildDirectory());
// Prepare application icon.
out.println("________Preparing application icon");
File resDir = createDir(buildDir, "res");
File drawableDir = createDir(resDir, "drawable");
if (!compiler.prepareApplicationIcon(new File(drawableDir, "ya.png"))) {
return false;
}
setProgress(15);
// Create anim directory and animation xml files
out.println("________Creating animation xml");
File animDir = createDir(resDir, "anim");
if (!compiler.createAnimationXml(animDir)) {
return false;
}
// Generate AndroidManifest.xml
out.println("________Generating manifest file");
File manifestFile = new File(buildDir, "AndroidManifest.xml");
if (!compiler.writeAndroidManifest(manifestFile)) {
return false;
}
setProgress(20);
// Insert native libraries
out.println("________Attaching native libraries");
if (!compiler.insertNativeLibs(buildDir)) {
return false;
}
// Add raw assets to sub-directory of project assets.
out.println("________Attaching component assets");
if (!compiler.attachCompAssets()) {
return false;
}
// Create class files.
out.println("________Compiling source files");
File classesDir = createDir(buildDir, "classes");
if (!compiler.generateClasses(classesDir)) {
return false;
}
setProgress(35);
// Invoke dx on class files
out.println("________Invoking DX");
// TODO(markf): Running DX is now pretty slow (~25 sec overhead the first time and ~15 sec
// overhead for subsequent runs). I think it's because of the need to dx the entire
// kawa runtime every time. We should probably only do that once and then copy all the
// kawa runtime dx files into the generated classes.dex (which would only contain the
// files compiled for this project).
// Aargh. It turns out that there's no way to manipulate .dex files to do the above. An
// Android guy suggested an alternate approach of shipping the kawa runtime .dex file as
// data with the application and then creating a new DexClassLoader using that .dex file
// and with the original app class loader as the parent of the new one.
// TODONE(zhuowei): Now using the new Android DX tool to merge dex files
// Needs to specify a writable cache dir on the command line that persists after shutdown
// Each pre-dexed file is identified via its MD5 hash (since the standard Android SDK's
// method of identifying via a hash of the path won't work when files
// are copied into temporary storage) and processed via a hacked up version of
// Android SDK's Dex Ant task
File tmpDir = createDirectory(buildDir, "tmp");
String dexedClassesDir = tmpDir.getAbsolutePath();
if (!compiler.runDx(classesDir, dexedClassesDir, false)) {
return false;
}
setProgress(85);
// Invoke aapt to package everything up
out.println("________Invoking AAPT");
File deployDir = createDir(buildDir, "deploy");
String tmpPackageName = deployDir.getAbsolutePath() + SLASH +
project.getProjectName() + ".ap_";
if (!compiler.runAaptPackage(manifestFile, resDir, tmpPackageName)) {
return false;
}
setProgress(90);
// Seal the apk with ApkBuilder
out.println("________Invoking ApkBuilder");
String apkAbsolutePath = deployDir.getAbsolutePath() + SLASH +
project.getProjectName() + ".apk";
if (!compiler.runApkBuilder(apkAbsolutePath, tmpPackageName, dexedClassesDir)) {
return false;
}
setProgress(95);
// Sign the apk file
out.println("________Signing the apk file");
if (!compiler.runJarSigner(apkAbsolutePath, keystoreFilePath)) {
return false;
}
// ZipAlign the apk file
out.println("________ZipAligning the apk file");
if (!compiler.runZipAlign(apkAbsolutePath, tmpDir)) {
return false;
}
setProgress(100);
out.println("Build finished in " +
((System.currentTimeMillis() - start) / 1000.0) + " seconds");
return true;
}
/*
* Creates all the animation xml files.
*/
private boolean createAnimationXml(File animDir) {
// Store the filenames, and their contents into a HashMap
// so that we can easily add more, and also to iterate
// through creating the files.
Map<String, String> files = new HashMap<String, String>();
files.put("fadein.xml", AnimationXmlConstants.FADE_IN_XML);
files.put("fadeout.xml", AnimationXmlConstants.FADE_OUT_XML);
files.put("hold.xml", AnimationXmlConstants.HOLD_XML);
files.put("zoom_enter.xml", AnimationXmlConstants.ZOOM_ENTER);
files.put("zoom_exit.xml", AnimationXmlConstants.ZOOM_EXIT);
files.put("zoom_enter_reverse.xml", AnimationXmlConstants.ZOOM_ENTER_REVERSE);
files.put("zoom_exit_reverse.xml", AnimationXmlConstants.ZOOM_EXIT_REVERSE);
files.put("slide_exit.xml", AnimationXmlConstants.SLIDE_EXIT);
files.put("slide_enter.xml", AnimationXmlConstants.SLIDE_ENTER);
files.put("slide_exit_reverse.xml", AnimationXmlConstants.SLIDE_EXIT_REVERSE);
files.put("slide_enter_reverse.xml", AnimationXmlConstants.SLIDE_ENTER_REVERSE);
files.put("slide_v_exit.xml", AnimationXmlConstants.SLIDE_V_EXIT);
files.put("slide_v_enter.xml", AnimationXmlConstants.SLIDE_V_ENTER);
files.put("slide_v_exit_reverse.xml", AnimationXmlConstants.SLIDE_V_EXIT_REVERSE);
files.put("slide_v_enter_reverse.xml", AnimationXmlConstants.SLIDE_V_ENTER_REVERSE);
for (String filename : files.keySet()) {
File file = new File(animDir, filename);
if (!writeXmlFile(file, files.get(filename))) {
return false;
}
}
return true;
}
/*
* Writes the given string input to the provided file.
*/
private boolean writeXmlFile(File file, String input) {
try {
BufferedWriter writer = new BufferedWriter(new FileWriter(file));
writer.write(input);
writer.close();
} catch (IOException e) {
e.printStackTrace();
return false;
}
return true;
}
/*
* Runs ApkBuilder by using the API instead of calling its main method because the main method
* can call System.exit(1), which will bring down our server.
*/
private boolean runApkBuilder(String apkAbsolutePath, String zipArchive, String dexedClassesDir) {
try {
ApkBuilder apkBuilder =
new ApkBuilder(apkAbsolutePath, zipArchive,
dexedClassesDir + File.separator + "classes.dex", null, System.out);
if (hasSecondDex) {
apkBuilder.addFile(new File(dexedClassesDir + File.separator + "classes2.dex"),
"classes2.dex");
}
apkBuilder.sealApk();
return true;
} catch (Exception e) {
// This is fatal.
e.printStackTrace();
LOG.warning("YAIL compiler - ApkBuilder failed.");
err.println("YAIL compiler - ApkBuilder failed.");
userErrors.print(String.format(ERROR_IN_STAGE, "ApkBuilder"));
return false;
}
}
/**
* Creates a new YAIL compiler.
*
* @param project project to build
* @param compTypes component types used in the project
* @param out stdout stream for compiler messages
* @param err stderr stream for compiler messages
* @param userErrors stream to write user-visible error messages
* @param childProcessMaxRam maximum RAM for child processes, in MBs.
*/
@VisibleForTesting
Compiler(Project project, Set<String> compTypes, PrintStream out, PrintStream err,
PrintStream userErrors, boolean isForCompanion,
int childProcessMaxRam, String dexCacheDir) {
this.project = project;
prepareCompTypes(compTypes);
readBuildInfo();
this.out = out;
this.err = err;
this.userErrors = userErrors;
this.isForCompanion = isForCompanion;
this.childProcessRamMb = childProcessMaxRam;
this.dexCacheDir = dexCacheDir;
}
/*
* Runs the Kawa compiler in a separate process to generate classes. Returns false if not able to
* create a class file for every source file in the project.
*
* As a side effect, we generate uniqueLibsNeeded which contains a set of libraries used by
* runDx. Each library appears in the set only once (which is why it is a set!). This is
* important because when we Dex the libraries, a given library can only appear once.
*
*/
private boolean generateClasses(File classesDir) {
try {
List<Project.SourceDescriptor> sources = project.getSources();
List<String> sourceFileNames = Lists.newArrayListWithCapacity(sources.size());
List<String> classFileNames = Lists.newArrayListWithCapacity(sources.size());
boolean userCodeExists = false;
for (Project.SourceDescriptor source : sources) {
String sourceFileName = source.getFile().getAbsolutePath();
LOG.log(Level.INFO, "source file: " + sourceFileName);
int srcIndex = sourceFileName.indexOf(File.separator + ".." + File.separator + "src" + File.separator);
String sourceFileRelativePath = sourceFileName.substring(srcIndex + 8);
String classFileName = (classesDir.getAbsolutePath() + "/" + sourceFileRelativePath)
.replace(YoungAndroidConstants.YAIL_EXTENSION, ".class");
// Check whether user code exists by seeing if a left parenthesis exists at the beginning of
// a line in the file
// TODO(user): Replace with more robust test of empty source file.
if (!userCodeExists) {
Reader fileReader = new FileReader(sourceFileName);
try {
while (fileReader.ready()) {
int c = fileReader.read();
if (c == '(') {
userCodeExists = true;
break;
}
}
} finally {
fileReader.close();
}
}
sourceFileNames.add(sourceFileName);
classFileNames.add(classFileName);
}
if (!userCodeExists) {
userErrors.print(NO_USER_CODE_ERROR);
return false;
}
// Construct the class path including component libraries (jars)
String classpath =
getResource(KAWA_RUNTIME) + COLON +
getResource(ACRA_RUNTIME) + COLON +
getResource(SIMPLE_ANDROID_RUNTIME_JAR) + COLON;
// attach the jars of external comps
for (String type : extCompTypes) {
String sourcePath = getExtCompDirPath(type) + SIMPLE_ANDROID_RUNTIME_JAR;
classpath += sourcePath + COLON;
}
// Add component library names to classpath
for (String type : libsNeeded.keySet()) {
for (String lib : libsNeeded.get(type)) {
String sourcePath = "";
String pathSuffix = RUNTIME_FILES_DIR + lib;
if (simpleCompTypes.contains(type)) {
sourcePath = getResource(pathSuffix);
} else if (extCompTypes.contains(type)) {
sourcePath = getExtCompDirPath(type) + pathSuffix;
} else {
userErrors.print(String.format(ERROR_IN_STAGE, "Compile"));
return false;
}
uniqueLibsNeeded.add(sourcePath);
classpath += sourcePath + COLON;
}
}
classpath +=
getResource(ANDROID_RUNTIME);
System.out.println("Libraries Classpath = " + classpath);
String yailRuntime = getResource(YAIL_RUNTIME);
List<String> kawaCommandArgs = Lists.newArrayList();
int mx = childProcessRamMb - 200;
Collections.addAll(kawaCommandArgs,
System.getProperty("java.home") + "/bin/java",
"-Dfile.encoding=UTF-8",
"-mx" + mx + "M",
"-cp", classpath,
"kawa.repl",
"-f", yailRuntime,
"-d", classesDir.getAbsolutePath(),
"-P", Signatures.getPackageName(project.getMainClass()) + ".",
"-C");
// TODO(lizlooney) - we are currently using (and have always used) absolute paths for the
// source file names. The resulting .class files contain references to the source file names,
// including the name of the tmp directory that contains them. We may be able to avoid that
// by using source file names that are relative to the project root and using the project
// root as the working directory for the Kawa compiler process.
kawaCommandArgs.addAll(sourceFileNames);
kawaCommandArgs.add(yailRuntime);
String[] kawaCommandLine = kawaCommandArgs.toArray(new String[kawaCommandArgs.size()]);
long start = System.currentTimeMillis();
// Capture Kawa compiler stderr. The ODE server parses out the warnings and errors and adds
// them to the protocol buffer for logging purposes. (See
// buildserver/ProjectBuilder.processCompilerOutout.
ByteArrayOutputStream kawaOutputStream = new ByteArrayOutputStream();
boolean kawaSuccess;
synchronized (SYNC_KAWA_OR_DX) {
kawaSuccess = Execution.execute(null, kawaCommandLine,
System.out, new PrintStream(kawaOutputStream));
}
if (!kawaSuccess) {
LOG.log(Level.SEVERE, "Kawa compile has failed.");
}
String kawaOutput = kawaOutputStream.toString();
out.print(kawaOutput);
String kawaCompileTimeMessage = "Kawa compile time: " +
((System.currentTimeMillis() - start) / 1000.0) + " seconds";
out.println(kawaCompileTimeMessage);
LOG.info(kawaCompileTimeMessage);
// Check that all of the class files were created.
// If they weren't, return with an error.
for (String classFileName : classFileNames) {
File classFile = new File(classFileName);
if (!classFile.exists()) {
LOG.log(Level.INFO, "Can't find class file: " + classFileName);
String screenName = classFileName.substring(classFileName.lastIndexOf('/') + 1,
classFileName.lastIndexOf('.'));
userErrors.print(String.format(COMPILATION_ERROR, screenName));
return false;
}
}
} catch (IOException e) {
e.printStackTrace();
userErrors.print(String.format(ERROR_IN_STAGE, "Compile"));
return false;
}
return true;
}
private boolean runJarSigner(String apkAbsolutePath, String keystoreAbsolutePath) {
// TODO(user): maybe make a command line flag for the jarsigner location
String javaHome = System.getProperty("java.home");
// This works on Mac OS X.
File jarsignerFile = new File(javaHome + SLASH + "bin" +
SLASH + "jarsigner");
if (!jarsignerFile.exists()) {
// This works when a JDK is installed with the JRE.
jarsignerFile = new File(javaHome + SLASH + ".." + SLASH + "bin" +
SLASH + "jarsigner");
if (System.getProperty("os.name").startsWith("Windows")) {
jarsignerFile = new File(javaHome + SLASH + ".." + SLASH + "bin" +
SLASH + "jarsigner.exe");
}
if (!jarsignerFile.exists()) {
LOG.warning("YAIL compiler - could not find jarsigner.");
err.println("YAIL compiler - could not find jarsigner.");
userErrors.print(String.format(ERROR_IN_STAGE, "JarSigner"));
return false;
}
}
String[] jarsignerCommandLine = {
jarsignerFile.getAbsolutePath(),
"-digestalg", "SHA1",
"-sigalg", "MD5withRSA",
"-keystore", keystoreAbsolutePath,
"-storepass", "android",
apkAbsolutePath,
"AndroidKey"
};
if (!Execution.execute(null, jarsignerCommandLine, System.out, System.err)) {
LOG.warning("YAIL compiler - jarsigner execution failed.");
err.println("YAIL compiler - jarsigner execution failed.");
userErrors.print(String.format(ERROR_IN_STAGE, "JarSigner"));
return false;
}
return true;
}
private boolean runZipAlign(String apkAbsolutePath, File tmpDir) {
// TODO(user): add zipalign tool appinventor->lib->android->tools->linux and windows
// Need to make sure assets directory exists otherwise zipalign will fail.
createDir(project.getAssetsDirectory());
String zipAlignTool;
String osName = System.getProperty("os.name");
if (osName.equals("Mac OS X")) {
zipAlignTool = MAC_ZIPALIGN_TOOL;
} else if (osName.equals("Linux")) {
zipAlignTool = LINUX_ZIPALIGN_TOOL;
} else if (osName.startsWith("Windows")) {
zipAlignTool = WINDOWS_ZIPALIGN_TOOL;
} else {
LOG.warning("YAIL compiler - cannot run ZIPALIGN on OS " + osName);
err.println("YAIL compiler - cannot run ZIPALIGN on OS " + osName);
userErrors.print(String.format(ERROR_IN_STAGE, "ZIPALIGN"));
return false;
}
// TODO: create tmp file for zipaling result
String zipAlignedPath = tmpDir.getAbsolutePath() + SLASH + "zipaligned.apk";
// zipalign -f -v 4 infile.zip outfile.zip
String[] zipAlignCommandLine = {
getResource(zipAlignTool),
"-f",
"4",
apkAbsolutePath,
zipAlignedPath
};
long startZipAlign = System.currentTimeMillis();
// Using System.err and System.out on purpose. Don't want to pollute build messages with
// tools output
if (!Execution.execute(null, zipAlignCommandLine, System.out, System.err)) {
LOG.warning("YAIL compiler - ZIPALIGN execution failed.");
err.println("YAIL compiler - ZIPALIGN execution failed.");
userErrors.print(String.format(ERROR_IN_STAGE, "ZIPALIGN"));
return false;
}
if (!copyFile(zipAlignedPath, apkAbsolutePath)) {
LOG.warning("YAIL compiler - ZIPALIGN file copy failed.");
err.println("YAIL compiler - ZIPALIGN file copy failed.");
userErrors.print(String.format(ERROR_IN_STAGE, "ZIPALIGN"));
return false;
}
String zipALignTimeMessage = "ZIPALIGN time: " +
((System.currentTimeMillis() - startZipAlign) / 1000.0) + " seconds";
out.println(zipALignTimeMessage);
LOG.info(zipALignTimeMessage);
return true;
}
/*
* Loads the icon for the application, either a user provided one or the default one.
*/
private boolean prepareApplicationIcon(File outputPngFile) {
String userSpecifiedIcon = Strings.nullToEmpty(project.getIcon());
try {
BufferedImage icon;
if (!userSpecifiedIcon.isEmpty()) {
File iconFile = new File(project.getAssetsDirectory(), userSpecifiedIcon);
icon = ImageIO.read(iconFile);
if (icon == null) {
// This can happen if the iconFile isn't an image file.
// For example, icon is null if the file is a .wav file.
// TODO(lizlooney) - This happens if the user specifies a .ico file. We should fix that.
userErrors.print(String.format(ICON_ERROR, userSpecifiedIcon));
return false;
}
} else {
// Load the default image.
icon = ImageIO.read(Compiler.class.getResource(DEFAULT_ICON));
}
ImageIO.write(icon, "png", outputPngFile);
} catch (Exception e) {
e.printStackTrace();
// If the user specified the icon, this is fatal.
if (!userSpecifiedIcon.isEmpty()) {
userErrors.print(String.format(ICON_ERROR, userSpecifiedIcon));
return false;
}
}
return true;
}
private boolean runDx(File classesDir, String dexedClassesDir, boolean secondTry) {
List<File> libList = new ArrayList<File>();
List<File> inputList = new ArrayList<File>();
List<File> class2List = new ArrayList<File>();
inputList.add(classesDir); //this is a directory, and won't be cached into the dex cache
inputList.add(new File(getResource(SIMPLE_ANDROID_RUNTIME_JAR)));
inputList.add(new File(getResource(KAWA_RUNTIME)));
inputList.add(new File(getResource(ACRA_RUNTIME)));
for (String lib : uniqueLibsNeeded) {
libList.add(new File(lib));
}
// BEGIN DEBUG -- XXX --
// System.err.println("runDx -- libraries");
// for (File aFile : inputList) {
// System.err.println(" inputList => " + aFile.getAbsolutePath());
// }
// for (File aFile : libList) {
// System.err.println(" libList => " + aFile.getAbsolutePath());
// }
// END DEBUG -- XXX --
// attach the jars of external comps to the libraries list
for (String type : extCompTypes) {
String sourcePath = getExtCompDirPath(type) + SIMPLE_ANDROID_RUNTIME_JAR;
libList.add(new File(sourcePath));
}
int offset = libList.size();
// Note: The choice of 12 libraries is arbitrary. We note that things
// worked to put all libraries into the first classes.dex file when we
// had 16 libraries and broke at 17. So this is a conservative number
// to try.
if (!secondTry) { // First time through, try base + 12 libraries
if (offset > 12)
offset = 12;
} else {
offset = 0; // Add NO libraries the second time through!
}
for (int i = 0; i < offset; i++) {
inputList.add(libList.get(i));
}
if (libList.size() - offset > 0) { // Any left over for classes2?
for (int i = offset; i < libList.size(); i++) {
class2List.add(libList.get(i));
}
}
DexExecTask dexTask = new DexExecTask();
dexTask.setExecutable(getResource(DX_JAR));
dexTask.setOutput(dexedClassesDir + File.separator + "classes.dex");
dexTask.setChildProcessRamMb(childProcessRamMb);
if (dexCacheDir == null) {
dexTask.setDisableDexMerger(true);
} else {
createDir(new File(dexCacheDir));
dexTask.setDexedLibs(dexCacheDir);
}
long startDx = System.currentTimeMillis();
// Using System.err and System.out on purpose. Don't want to pollute build messages with
// tools output
boolean dxSuccess;
synchronized (SYNC_KAWA_OR_DX) {
setProgress(50);
dxSuccess = dexTask.execute(inputList);
if (dxSuccess && (class2List.size() > 0)) {
setProgress(60);
dexTask.setOutput(dexedClassesDir + File.separator + "classes2.dex");
inputList = new ArrayList<File>();
dxSuccess = dexTask.execute(class2List);
setProgress(75);
hasSecondDex = true;
} else if (!dxSuccess) { // The initial dx blew out, try more conservative
LOG.info("DX execution failed, trying with fewer libraries.");
if (secondTry) { // Already tried the more conservative approach!
LOG.warning("YAIL compiler - DX execution failed (secondTry!).");
err.println("YAIL compiler - DX execution failed.");
userErrors.print(String.format(ERROR_IN_STAGE, "DX"));
return false;
} else {
return runDx(classesDir, dexedClassesDir, true);
}
}
}
if (!dxSuccess) {
LOG.warning("YAIL compiler - DX execution failed.");
err.println("YAIL compiler - DX execution failed.");
userErrors.print(String.format(ERROR_IN_STAGE, "DX"));
return false;
}
String dxTimeMessage = "DX time: " +
((System.currentTimeMillis() - startDx) / 1000.0) + " seconds";
out.println(dxTimeMessage);
LOG.info(dxTimeMessage);
return true;
}
private boolean runAaptPackage(File manifestFile, File resDir, String tmpPackageName) {
// Need to make sure assets directory exists otherwise aapt will fail.
createDir(project.getAssetsDirectory());
String aaptTool;
String osName = System.getProperty("os.name");
if (osName.equals("Mac OS X")) {
aaptTool = MAC_AAPT_TOOL;
} else if (osName.equals("Linux")) {
aaptTool = LINUX_AAPT_TOOL;
} else if (osName.startsWith("Windows")) {
aaptTool = WINDOWS_AAPT_TOOL;
} else {
LOG.warning("YAIL compiler - cannot run AAPT on OS " + osName);
err.println("YAIL compiler - cannot run AAPT on OS " + osName);
userErrors.print(String.format(ERROR_IN_STAGE, "AAPT"));
return false;
}
String[] aaptPackageCommandLine = {
getResource(aaptTool),
"package",
"-v",
"-f",
"-M", manifestFile.getAbsolutePath(),
"-S", resDir.getAbsolutePath(),
"-A", project.getAssetsDirectory().getAbsolutePath(),
"-I", getResource(ANDROID_RUNTIME),
"-F", tmpPackageName,
libsDir.getAbsolutePath()
};
long startAapt = System.currentTimeMillis();
// Using System.err and System.out on purpose. Don't want to pollute build messages with
// tools output
if (!Execution.execute(null, aaptPackageCommandLine, System.out, System.err)) {
LOG.warning("YAIL compiler - AAPT execution failed.");
err.println("YAIL compiler - AAPT execution failed.");
userErrors.print(String.format(ERROR_IN_STAGE, "AAPT"));
return false;
}
String aaptTimeMessage = "AAPT time: " +
((System.currentTimeMillis() - startAapt) / 1000.0) + " seconds";
out.println(aaptTimeMessage);
LOG.info(aaptTimeMessage);
return true;
}
private boolean insertNativeLibs(File buildDir){
/**
* Native libraries are targeted for particular processor architectures.
* Here, non-default architectures (ARMv5TE is default) are identified with suffixes
* before being placed in the appropriate directory with their suffix removed.
*/
libsDir = createDir(buildDir, LIBS_DIR_NAME);
File armeabiDir = createDir(libsDir, ARMEABI_DIR_NAME);
File armeabiV7aDir = createDir(libsDir, ARMEABI_V7A_DIR_NAME);
try {
for (String type : nativeLibsNeeded.keySet()) {
for (String lib : nativeLibsNeeded.get(type)) {
boolean isV7a = lib.endsWith(ARMEABI_V7A_SUFFIX);
String sourceDirName = isV7a ? ARMEABI_V7A_DIR_NAME : ARMEABI_DIR_NAME;
File targetDir = isV7a ? armeabiV7aDir : armeabiDir;
lib = isV7a ? lib.substring(0, lib.length() - ARMEABI_V7A_SUFFIX.length()) : lib;
String sourcePath = "";
String pathSuffix = RUNTIME_FILES_DIR + sourceDirName + SLASH + lib;
if (simpleCompTypes.contains(type)) {
sourcePath = getResource(pathSuffix);
} else if (extCompTypes.contains(type)) {
sourcePath = getExtCompDirPath(type) + pathSuffix;
targetDir = createDir(targetDir, EXT_COMPS_DIR_NAME);
targetDir = createDir(targetDir, type);
} else {
userErrors.print(String.format(ERROR_IN_STAGE, "Native Code"));
return false;
}
Files.copy(new File(sourcePath), new File(targetDir, lib));
}
}
return true;
} catch (IOException e) {
e.printStackTrace();
userErrors.print(String.format(ERROR_IN_STAGE, "Native Code"));
return false;
}
}
private boolean attachCompAssets() {
createDir(project.getAssetsDirectory()); // Needed to insert resources.
try {
// Gather non-library assets to be added to apk's Asset directory.
// The assets directory have been created before this.
File compAssetDir = createDir(project.getAssetsDirectory(),
ASSET_DIRECTORY);
for (String type : assetsNeeded.keySet()) {
for (String assetName : assetsNeeded.get(type)) {
File targetDir = compAssetDir;
String sourcePath = "";
String pathSuffix = RUNTIME_FILES_DIR + assetName;
if (simpleCompTypes.contains(type)) {
sourcePath = getResource(pathSuffix);
} else if (extCompTypes.contains(type)) {
sourcePath = getExtCompDirPath(type) + pathSuffix;
targetDir = createDir(targetDir, EXT_COMPS_DIR_NAME);
targetDir = createDir(targetDir, type);
} else {
userErrors.print(String.format(ERROR_IN_STAGE, "Assets"));
return false;
}
Files.copy(new File(sourcePath), new File(targetDir, assetName));
}
}
return true;
} catch (IOException e) {
e.printStackTrace();
userErrors.print(String.format(ERROR_IN_STAGE, "Assets"));
return false;
}
}
/**
* Writes out the given resource as a temp file and returns the absolute path.
* Caches the location of the files, so we can reuse them.
*
* @param resourcePath the name of the resource
*/
static synchronized String getResource(String resourcePath) {
try {
File file = resources.get(resourcePath);
if (file == null) {
String basename = PathUtil.basename(resourcePath);
String prefix;
String suffix;
int lastDot = basename.lastIndexOf(".");
if (lastDot != -1) {
prefix = basename.substring(0, lastDot);
suffix = basename.substring(lastDot);
} else {
prefix = basename;
suffix = "";
}
while (prefix.length() < 3) {
prefix = prefix + "_";
}
file = File.createTempFile(prefix, suffix);
file.setExecutable(true);
file.deleteOnExit();
file.getParentFile().mkdirs();
Files.copy(Resources.newInputStreamSupplier(Compiler.class.getResource(resourcePath)),
file);
resources.put(resourcePath, file);
}
return file.getAbsolutePath();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
/*
* Loads permissions and information on component libraries and assets.
*/
private void loadJsonInfo(ConcurrentMap<String, Set<String>> infoMap, String targetInfo)
throws IOException, JSONException {
synchronized (infoMap) {
if (!infoMap.isEmpty()) {
return;
}
JSONArray buildInfo = new JSONArray(
"[" + simpleCompsBuildInfo.join(",") + "," +
extCompsBuildInfo.join(",") + "]");
for (int i = 0; i < buildInfo.length(); ++i) {
JSONObject compJson = buildInfo.getJSONObject(i);
JSONArray infoArray = null;
String type = compJson.getString("type");
try {
infoArray = compJson.getJSONArray(targetInfo);
} catch (JSONException e) {
// Older compiled extensions will not have a broadcastReceiver
// defined. Rather then require them all to be recompiled, we
// treat the missing attribute as empty.
if (e.getMessage().contains("broadcastReceiver")) {
LOG.log(Level.INFO, "Component \"" + type + "\" does not have a broadcast receiver.");
continue;
} else {
throw e;
}
}
if (!simpleCompTypes.contains(type) && !extCompTypes.contains(type)) {
continue;
}
Set<String> infoSet = Sets.newHashSet();
for (int j = 0; j < infoArray.length(); ++j) {
String info = infoArray.getString(j);
infoSet.add(info);
}
if (!infoSet.isEmpty()) {
infoMap.put(type, infoSet);
}
}
}
}
/**
* Copy one file to another. If destination file does not exist, it is created.
*
* @param srcPath absolute path to source file
* @param dstPath absolute path to destination file
* @return {@code true} if the copy succeeds, {@code false} otherwise
*/
private static Boolean copyFile(String srcPath, String dstPath) {
try {
FileInputStream in = new FileInputStream(srcPath);
FileOutputStream out = new FileOutputStream(dstPath);
byte[] buf = new byte[1024];
int len;
while ((len = in.read(buf)) > 0) {
out.write(buf, 0, len);
}
in.close();
out.close();
}
catch (IOException e) {
e.printStackTrace();
return false;
}
return true;
}
/**
* Creates a new directory (if it doesn't exist already).
*
* @param dir new directory
* @return new directory
*/
private static File createDir(File dir) {
if (!dir.exists()) {
dir.mkdir();
}
return dir;
}
/**
* Creates a new directory (if it doesn't exist already).
*
* @param parentDir parent directory of new directory
* @param name name of new directory
* @return new directory
*/
private static File createDir(File parentDir, String name) {
File dir = new File(parentDir, name);
if (!dir.exists()) {
dir.mkdir();
}
return dir;
}
/**
* Creates a new directory (if it doesn't exist already).
*
* @param parentDirectory parent directory of new directory
* @param name name of new directory
* @return new directory
*/
private static File createDirectory(File parentDirectory, String name) {
File dir = new File(parentDirectory, name);
if (!dir.exists()) {
dir.mkdir();
}
return dir;
}
private static int setProgress(int increments) {
Compiler.currentProgress = increments;
LOG.info("The current progress is "
+ Compiler.currentProgress + "%");
return Compiler.currentProgress;
}
public static int getProgress() {
if (Compiler.currentProgress==100) {
Compiler.currentProgress = 10;
return 100;
} else {
return Compiler.currentProgress;
}
}
private void readBuildInfo() {
try {
simpleCompsBuildInfo = new JSONArray(Resources.toString(
Compiler.class.getResource(COMP_BUILD_INFO), Charsets.UTF_8));
extCompsBuildInfo = new JSONArray();
for (String type : extCompTypes) {
// .../assets/external_comps/com.package.MyExtComp/files/component_build_info.json
File extCompRuntimeFileDir = new File(getExtCompDirPath(type) + RUNTIME_FILES_DIR);
if (!extCompRuntimeFileDir.exists()) {
// try extension package name for multi-extension files
String path = getExtCompDirPath(type);
path = path.substring(0, path.lastIndexOf('.'));
extCompRuntimeFileDir = new File(path + RUNTIME_FILES_DIR);
}
String jsonFileName = "component_build_info.json";
File jsonFile = new File(extCompRuntimeFileDir, jsonFileName);
String buildInfo = Resources.toString(jsonFile.toURI().toURL(), Charsets.UTF_8);
JSONTokener tokener = new JSONTokener(buildInfo);
Object value = tokener.nextValue();
if (value instanceof JSONObject) {
extCompsBuildInfo.put((JSONObject) value);
} else if (value instanceof JSONArray) {
JSONArray infos = (JSONArray) value;
for (int i = 0; i < infos.length(); i++) {
extCompsBuildInfo.put(infos.getJSONObject(i));
}
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
private void prepareCompTypes(Set<String> neededTypes) {
try {
JSONArray buildInfo = new JSONArray(Resources.toString(
Compiler.class.getResource(COMP_BUILD_INFO), Charsets.UTF_8));
Set<String> allSimpleTypes = Sets.newHashSet();
for (int i = 0; i < buildInfo.length(); ++i) {
JSONObject comp = buildInfo.getJSONObject(i);
allSimpleTypes.add(comp.getString("type"));
}
simpleCompTypes = Sets.newHashSet(neededTypes);
simpleCompTypes.retainAll(allSimpleTypes);
extCompTypes = Sets.newHashSet(neededTypes);
extCompTypes.removeAll(allSimpleTypes);
} catch (Exception e) {
e.printStackTrace();
}
}
private String getExtCompDirPath(String type) {
createDir(project.getAssetsDirectory());
return project.getAssetsDirectory().getAbsolutePath() + SLASH +
EXT_COMPS_DIR_NAME + SLASH + type.substring(0, type.lastIndexOf('.'));
}
}