aboutsummaryrefslogtreecommitdiff
path: root/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build
diff options
context:
space:
mode:
Diffstat (limited to 'eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build')
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/AaptExecException.java29
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/AaptParser.java783
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/AaptQuickFix.java431
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/AaptResultException.java29
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/AidlProcessor.java484
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/BuildHelper.java1225
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/ConvertSwitchDialog.java144
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/ConvertSwitchQuickFixProcessor.java224
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/DefaultSourceChangeHandler.java105
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/DexException.java33
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/DexWrapper.java218
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/ExecResultException.java72
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/Messages.java150
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/NativeLibInJarException.java61
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/ProguardExecException.java30
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/ProguardResultException.java34
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/RenderScriptLauncher.java244
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/RsSourceChangeHandler.java90
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/SourceChangeHandler.java25
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/SourceFileData.java132
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/SourceProcessor.java454
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/build_messages.properties67
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/builders/BaseBuilder.java483
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/builders/ChangedFileSet.java70
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/builders/ChangedFileSetHelper.java200
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/builders/JavaResChangedSet.java46
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/builders/PatternBasedDeltaVisitor.java139
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/builders/PostCompilerBuilder.java946
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/builders/PreCompilerBuilder.java1401
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/builders/PreCompilerDeltaVisitor.java417
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/builders/ResourceManagerBuilder.java317
31 files changed, 9083 insertions, 0 deletions
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/AaptExecException.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/AaptExecException.java
new file mode 100644
index 000000000..23b1baa92
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/AaptExecException.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.internal.build;
+
+/**
+ * Exception thrown when the execution of aapt fails.
+ *
+ */
+public final class AaptExecException extends Exception {
+ private static final long serialVersionUID = 1L;
+
+ AaptExecException(String message, Throwable cause) {
+ super(message, cause);
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/AaptParser.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/AaptParser.java
new file mode 100644
index 000000000..1f17fb7c6
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/AaptParser.java
@@ -0,0 +1,783 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.internal.build;
+
+import com.android.ide.eclipse.adt.AdtConstants;
+import com.android.ide.eclipse.adt.AdtPlugin;
+import com.android.ide.eclipse.adt.internal.project.BaseProjectHelper;
+
+import org.eclipse.core.resources.IFile;
+import org.eclipse.core.resources.IMarker;
+import org.eclipse.core.resources.IProject;
+import org.eclipse.core.resources.IResource;
+import org.eclipse.core.runtime.CoreException;
+import org.eclipse.jface.text.FindReplaceDocumentAdapter;
+import org.eclipse.jface.text.IDocument;
+import org.eclipse.jface.text.IRegion;
+import org.eclipse.jface.text.Region;
+import org.eclipse.ui.editors.text.TextFileDocumentProvider;
+import org.eclipse.ui.texteditor.IDocumentProvider;
+
+import java.io.File;
+import java.util.List;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+public final class AaptParser {
+
+ // TODO: rename the pattern to something that makes sense + javadoc comments.
+ /**
+ * Single line aapt warning for skipping files.<br>
+ * " (skipping hidden file '&lt;file path&gt;'"
+ */
+ private final static Pattern sPattern0Line1 = Pattern.compile(
+ "^\\s+\\(skipping hidden file\\s'(.*)'\\)$"); //$NON-NLS-1$
+
+ /**
+ * First line of dual line aapt error.<br>
+ * "ERROR at line &lt;line&gt;: &lt;error&gt;"<br>
+ * " (Occurred while parsing &lt;path&gt;)"
+ */
+ private final static Pattern sPattern1Line1 = Pattern.compile(
+ "^ERROR\\s+at\\s+line\\s+(\\d+):\\s+(.*)$"); //$NON-NLS-1$
+ /**
+ * Second line of dual line aapt error.<br>
+ * "ERROR at line &lt;line&gt;: &lt;error&gt;"<br>
+ * " (Occurred while parsing &lt;path&gt;)"<br>
+ * @see #sPattern1Line1
+ */
+ private final static Pattern sPattern1Line2 = Pattern.compile(
+ "^\\s+\\(Occurred while parsing\\s+(.*)\\)$"); //$NON-NLS-1$
+ /**
+ * First line of dual line aapt error.<br>
+ * "ERROR: &lt;error&gt;"<br>
+ * "Defined at file &lt;path&gt; line &lt;line&gt;"
+ */
+ private final static Pattern sPattern2Line1 = Pattern.compile(
+ "^ERROR:\\s+(.+)$"); //$NON-NLS-1$
+ /**
+ * Second line of dual line aapt error.<br>
+ * "ERROR: &lt;error&gt;"<br>
+ * "Defined at file &lt;path&gt; line &lt;line&gt;"<br>
+ * @see #sPattern2Line1
+ */
+ private final static Pattern sPattern2Line2 = Pattern.compile(
+ "Defined\\s+at\\s+file\\s+(.+)\\s+line\\s+(\\d+)"); //$NON-NLS-1$
+ /**
+ * Single line aapt error<br>
+ * "&lt;path&gt; line &lt;line&gt;: &lt;error&gt;"
+ */
+ private final static Pattern sPattern3Line1 = Pattern.compile(
+ "^(.+)\\sline\\s(\\d+):\\s(.+)$"); //$NON-NLS-1$
+ /**
+ * First line of dual line aapt error.<br>
+ * "ERROR parsing XML file &lt;path&gt;"<br>
+ * "&lt;error&gt; at line &lt;line&gt;"
+ */
+ private final static Pattern sPattern4Line1 = Pattern.compile(
+ "^Error\\s+parsing\\s+XML\\s+file\\s(.+)$"); //$NON-NLS-1$
+ /**
+ * Second line of dual line aapt error.<br>
+ * "ERROR parsing XML file &lt;path&gt;"<br>
+ * "&lt;error&gt; at line &lt;line&gt;"<br>
+ * @see #sPattern4Line1
+ */
+ private final static Pattern sPattern4Line2 = Pattern.compile(
+ "^(.+)\\s+at\\s+line\\s+(\\d+)$"); //$NON-NLS-1$
+
+ /**
+ * Single line aapt warning<br>
+ * "&lt;path&gt;:&lt;line&gt;: &lt;error&gt;"
+ */
+ private final static Pattern sPattern5Line1 = Pattern.compile(
+ "^(.+?):(\\d+):\\s+WARNING:(.+)$"); //$NON-NLS-1$
+
+ /**
+ * Single line aapt error<br>
+ * "&lt;path&gt;:&lt;line&gt;: &lt;error&gt;"
+ */
+ private final static Pattern sPattern6Line1 = Pattern.compile(
+ "^(.+?):(\\d+):\\s+(.+)$"); //$NON-NLS-1$
+
+ /**
+ * 4 line aapt error<br>
+ * "ERROR: 9-path image &lt;path&gt; malformed"<br>
+ * Line 2 and 3 are taken as-is while line 4 is ignored (it repeats with<br>
+ * 'ERROR: failure processing &lt;path&gt;)
+ */
+ private final static Pattern sPattern7Line1 = Pattern.compile(
+ "^ERROR:\\s+9-patch\\s+image\\s+(.+)\\s+malformed\\.$"); //$NON-NLS-1$
+
+ private final static Pattern sPattern8Line1 = Pattern.compile(
+ "^(invalid resource directory name): (.*)$"); //$NON-NLS-1$
+
+ /**
+ * Portion of the error message which states the context in which the error occurred,
+ * such as which property was being processed and what the string value was that
+ * caused the error.
+ * <p>
+ * Example:
+ * error: No resource found that matches the given name (at 'text' with value '@string/foo')
+ */
+ private static final Pattern sValueRangePattern =
+ Pattern.compile("\\(at '(.+)' with value '(.*)'\\)"); //$NON-NLS-1$
+
+
+ /**
+ * Portion of error message which points to the second occurrence of a repeated resource
+ * definition.
+ * <p>
+ * Example:
+ * error: Resource entry repeatedStyle1 already has bag item android:gravity.
+ */
+ private static final Pattern sRepeatedRangePattern =
+ Pattern.compile("Resource entry (.+) already has bag item (.+)\\."); //$NON-NLS-1$
+
+ /**
+ * Error message emitted when aapt skips a file because for example it's name is
+ * invalid, such as a layout file name which starts with _.
+ * <p>
+ * This error message is used by AAPT in Tools 19 and earlier.
+ */
+ private static final Pattern sSkippingPattern =
+ Pattern.compile(" \\(skipping (.+) .+ '(.*)'\\)"); //$NON-NLS-1$
+
+ /**
+ * Error message emitted when aapt skips a file because for example it's name is
+ * invalid, such as a layout file name which starts with _.
+ * <p>
+ * This error message is used by AAPT in Tools 20 and later.
+ */
+ private static final Pattern sNewSkippingPattern =
+ Pattern.compile(" \\(skipping .+ '(.+)' due to ANDROID_AAPT_IGNORE pattern '.+'\\)"); //$NON-NLS-1$
+
+ /**
+ * Suffix of error message which points to the first occurrence of a repeated resource
+ * definition.
+ * Example:
+ * Originally defined here.
+ */
+ private static final String ORIGINALLY_DEFINED_MSG = "Originally defined here."; //$NON-NLS-1$
+
+ /**
+ * Portion of error message which points to the second occurrence of a repeated resource
+ * definition.
+ * <p>
+ * Example:
+ * error: Resource entry repeatedStyle1 already has bag item android:gravity.
+ */
+ private static final Pattern sNoResourcePattern =
+ Pattern.compile("No resource found that matches the given name: attr '(.+)'\\."); //$NON-NLS-1$
+
+ /**
+ * Portion of error message which points to a missing required attribute in a
+ * resource definition.
+ * <p>
+ * Example:
+ * error: error: A 'name' attribute is required for <style>
+ */
+ private static final Pattern sRequiredPattern =
+ Pattern.compile("A '(.+)' attribute is required for <(.+)>"); //$NON-NLS-1$
+
+ /**
+ * 2 line aapt error<br>
+ * "ERROR: Invalid configuration: foo"<br>
+ * " ^^^"<br>
+ * There's no need to parse the 2nd line.
+ */
+ private final static Pattern sPattern9Line1 = Pattern.compile(
+ "^Invalid configuration: (.+)$"); //$NON-NLS-1$
+
+ private final static Pattern sXmlBlockPattern = Pattern.compile(
+ "W/ResourceType\\(.*\\): Bad XML block: no root element node found"); //$NON-NLS-1$
+
+ /**
+ * Parse the output of aapt and mark the incorrect file with error markers
+ *
+ * @param results the output of aapt
+ * @param project the project containing the file to mark
+ * @return true if the parsing failed, false if success.
+ */
+ public static boolean parseOutput(List<String> results, IProject project) {
+ int size = results.size();
+ if (size > 0) {
+ return parseOutput(results.toArray(new String[size]), project);
+ }
+
+ return false;
+ }
+
+ /**
+ * Parse the output of aapt and mark the incorrect file with error markers
+ *
+ * @param results the output of aapt
+ * @param project the project containing the file to mark
+ * @return true if the parsing failed, false if success.
+ */
+ public static boolean parseOutput(String[] results, IProject project) {
+ // nothing to parse? just return false;
+ if (results.length == 0) {
+ return false;
+ }
+
+ // get the root of the project so that we can make IFile from full
+ // file path
+ String osRoot = project.getLocation().toOSString();
+
+ Matcher m;
+
+ for (int i = 0; i < results.length ; i++) {
+ String p = results[i];
+
+ m = sPattern0Line1.matcher(p);
+ if (m.matches()) {
+ // we ignore those (as this is an ignore message from aapt)
+ continue;
+ }
+
+ m = sPattern1Line1.matcher(p);
+ if (m.matches()) {
+ String lineStr = m.group(1);
+ String msg = m.group(2);
+
+ // get the matcher for the next line.
+ m = getNextLineMatcher(results, ++i, sPattern1Line2);
+ if (m == null) {
+ return true;
+ }
+
+ String location = m.group(1);
+
+ // check the values and attempt to mark the file.
+ if (checkAndMark(location, lineStr, msg, osRoot, project,
+ AdtConstants.MARKER_AAPT_COMPILE, IMarker.SEVERITY_ERROR) == false) {
+ return true;
+ }
+ continue;
+ }
+
+ // this needs to be tested before Pattern2 since they both start with 'ERROR:'
+ m = sPattern7Line1.matcher(p);
+ if (m.matches()) {
+ String location = m.group(1);
+ String msg = p; // default msg is the line in case we don't find anything else
+
+ if (++i < results.length) {
+ msg = results[i].trim();
+ if (++i < results.length) {
+ msg = msg + " - " + results[i].trim(); //$NON-NLS-1$
+
+ // skip the next line
+ i++;
+ }
+ }
+
+ // display the error
+ if (checkAndMark(location, null, msg, osRoot, project,
+ AdtConstants.MARKER_AAPT_COMPILE, IMarker.SEVERITY_ERROR) == false) {
+ return true;
+ }
+
+ // success, go to the next line
+ continue;
+ }
+
+ m = sPattern2Line1.matcher(p);
+ if (m.matches()) {
+ // get the msg
+ String msg = m.group(1);
+
+ // get the matcher for the next line.
+ m = getNextLineMatcher(results, ++i, sPattern2Line2);
+ if (m == null) {
+ return true;
+ }
+
+ String location = m.group(1);
+ String lineStr = m.group(2);
+
+ // check the values and attempt to mark the file.
+ if (checkAndMark(location, lineStr, msg, osRoot, project,
+ AdtConstants.MARKER_AAPT_COMPILE, IMarker.SEVERITY_ERROR) == false) {
+ return true;
+ }
+ continue;
+ }
+
+ m = sPattern3Line1.matcher(p);
+ if (m.matches()) {
+ String location = m.group(1);
+ String lineStr = m.group(2);
+ String msg = m.group(3);
+
+ // check the values and attempt to mark the file.
+ if (checkAndMark(location, lineStr, msg, osRoot, project,
+ AdtConstants.MARKER_AAPT_COMPILE, IMarker.SEVERITY_ERROR) == false) {
+ return true;
+ }
+
+ // success, go to the next line
+ continue;
+ }
+
+ m = sPattern4Line1.matcher(p);
+ if (m.matches()) {
+ // get the filename.
+ String location = m.group(1);
+
+ // get the matcher for the next line.
+ m = getNextLineMatcher(results, ++i, sPattern4Line2);
+ if (m == null) {
+ return true;
+ }
+
+ String msg = m.group(1);
+ String lineStr = m.group(2);
+
+ // check the values and attempt to mark the file.
+ if (checkAndMark(location, lineStr, msg, osRoot, project,
+ AdtConstants.MARKER_AAPT_COMPILE, IMarker.SEVERITY_ERROR) == false) {
+ return true;
+ }
+
+ // success, go to the next line
+ continue;
+ }
+
+ m = sPattern5Line1.matcher(p);
+ if (m.matches()) {
+ String location = m.group(1);
+ String lineStr = m.group(2);
+ String msg = m.group(3);
+
+ // check the values and attempt to mark the file.
+ if (checkAndMark(location, lineStr, msg, osRoot, project,
+ AdtConstants.MARKER_AAPT_COMPILE, IMarker.SEVERITY_WARNING) == false) {
+ return true;
+ }
+
+ // success, go to the next line
+ continue;
+ }
+
+ m = sPattern6Line1.matcher(p);
+ if (m.matches()) {
+ String location = m.group(1);
+ String lineStr = m.group(2);
+ String msg = m.group(3);
+
+ // check the values and attempt to mark the file.
+ if (checkAndMark(location, lineStr, msg, osRoot, project,
+ AdtConstants.MARKER_AAPT_COMPILE, IMarker.SEVERITY_ERROR) == false) {
+ return true;
+ }
+
+ // success, go to the next line
+ continue;
+ }
+
+ m = sPattern8Line1.matcher(p);
+ if (m.matches()) {
+ String location = m.group(2);
+ String msg = m.group(1);
+
+ // check the values and attempt to mark the file.
+ if (checkAndMark(location, null, msg, osRoot, project,
+ AdtConstants.MARKER_AAPT_COMPILE, IMarker.SEVERITY_ERROR) == false) {
+ return true;
+ }
+
+ // success, go to the next line
+ continue;
+ }
+
+ m = sPattern9Line1.matcher(p);
+ if (m.matches()) {
+ String badConfig = m.group(1);
+ String msg = String.format("APK Configuration filter '%1$s' is invalid", badConfig);
+
+ // skip the next line
+ i++;
+
+ // check the values and attempt to mark the file.
+ if (checkAndMark(null /*location*/, null, msg, osRoot, project,
+ AdtConstants.MARKER_AAPT_PACKAGE, IMarker.SEVERITY_ERROR) == false) {
+ return true;
+ }
+
+ // success, go to the next line
+ continue;
+ }
+
+ m = sNewSkippingPattern.matcher(p);
+ if (m.matches()) {
+ String location = m.group(1);
+
+ if (location.startsWith(".") //$NON-NLS-1$
+ || location.endsWith("~")) { //$NON-NLS-1$
+ continue;
+ }
+
+ // check the values and attempt to mark the file.
+ if (checkAndMark(location, null, p.trim(), osRoot, project,
+ AdtConstants.MARKER_AAPT_COMPILE, IMarker.SEVERITY_WARNING) == false) {
+ return true;
+ }
+
+ // success, go to the next line
+ continue;
+ }
+
+ m = sSkippingPattern.matcher(p);
+ if (m.matches()) {
+ String location = m.group(2);
+
+ // Certain files can safely be skipped without marking the project
+ // as having errors. See isHidden() in AaptAssets.cpp:
+ String type = m.group(1);
+ if (type.equals("backup") //$NON-NLS-1$ // main.xml~, etc
+ || type.equals("hidden") //$NON-NLS-1$ // .gitignore, etc
+ || type.equals("index")) { //$NON-NLS-1$ // thumbs.db, etc
+ continue;
+ }
+
+ // check the values and attempt to mark the file.
+ if (checkAndMark(location, null, p.trim(), osRoot, project,
+ AdtConstants.MARKER_AAPT_COMPILE, IMarker.SEVERITY_WARNING) == false) {
+ return true;
+ }
+
+ // success, go to the next line
+ continue;
+ }
+
+ m = sXmlBlockPattern.matcher(p);
+ if (m.matches()) {
+ // W/ResourceType(12345): Bad XML block: no root element node found
+ // Sadly there's NO filename reference; this error typically describes the
+ // error *after* this line.
+ if (results.length == 1) {
+ // This is the only error message: dump to console and quit
+ return true;
+ }
+ // Continue: the real culprit is displayed next and should get a marker
+ continue;
+ }
+
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Check if the parameters gotten from the error output are valid, and mark
+ * the file with an AAPT marker.
+ * @param location the full OS path of the error file. If null, the project is marked
+ * @param lineStr
+ * @param message
+ * @param root The root directory of the project, in OS specific format.
+ * @param project
+ * @param markerId The marker id to put.
+ * @param severity The severity of the marker to put (IMarker.SEVERITY_*)
+ * @return true if the parameters were valid and the file was marked successfully.
+ *
+ * @see IMarker
+ */
+ private static final boolean checkAndMark(String location, String lineStr,
+ String message, String root, IProject project, String markerId, int severity) {
+ // check this is in fact a file
+ if (location != null) {
+ File f = new File(location);
+ if (f.exists() == false) {
+ return false;
+ }
+ }
+
+ // get the line number
+ int line = -1; // default value for error with no line.
+
+ if (lineStr != null) {
+ try {
+ line = Integer.parseInt(lineStr);
+ } catch (NumberFormatException e) {
+ // looks like the string we extracted wasn't a valid
+ // file number. Parsing failed and we return true
+ return false;
+ }
+ }
+
+ // add the marker
+ IResource f2 = project;
+ if (location != null) {
+ f2 = getResourceFromFullPath(location, root, project);
+ if (f2 == null) {
+ return false;
+ }
+ }
+
+ // Attempt to determine the exact range of characters affected by this error.
+ // This will look up the actual text of the file, go to the particular error line
+ // and scan for the specific string mentioned in the error.
+ int startOffset = -1;
+ int endOffset = -1;
+ if (f2 instanceof IFile) {
+ IRegion region = findRange((IFile) f2, line, message);
+ if (region != null) {
+ startOffset = region.getOffset();
+ endOffset = startOffset + region.getLength();
+ }
+ }
+
+ // check if there's a similar marker already, since aapt is launched twice
+ boolean markerAlreadyExists = false;
+ try {
+ IMarker[] markers = f2.findMarkers(markerId, true, IResource.DEPTH_ZERO);
+
+ for (IMarker marker : markers) {
+ if (startOffset != -1) {
+ int tmpBegin = marker.getAttribute(IMarker.CHAR_START, -1);
+ if (tmpBegin != startOffset) {
+ break;
+ }
+ int tmpEnd = marker.getAttribute(IMarker.CHAR_END, -1);
+ if (tmpEnd != startOffset) {
+ break;
+ }
+ }
+
+ int tmpLine = marker.getAttribute(IMarker.LINE_NUMBER, -1);
+ if (tmpLine != line) {
+ break;
+ }
+
+ int tmpSeverity = marker.getAttribute(IMarker.SEVERITY, -1);
+ if (tmpSeverity != severity) {
+ break;
+ }
+
+ String tmpMsg = marker.getAttribute(IMarker.MESSAGE, null);
+ if (tmpMsg == null || tmpMsg.equals(message) == false) {
+ break;
+ }
+
+ // if we're here, all the marker attributes are equals, we found it
+ // and exit
+ markerAlreadyExists = true;
+ break;
+ }
+
+ } catch (CoreException e) {
+ // if we couldn't get the markers, then we just mark the file again
+ // (since markerAlreadyExists is initialized to false, we do nothing)
+ }
+
+ if (markerAlreadyExists == false) {
+ BaseProjectHelper.markResource(f2, markerId, message, line,
+ startOffset, endOffset, severity);
+ }
+
+ return true;
+ }
+
+ /**
+ * Given an aapt error message in a given file and a given (initial) line number,
+ * return the corresponding offset range for the error, or null.
+ */
+ private static IRegion findRange(IFile file, int line, String message) {
+ Matcher matcher = sValueRangePattern.matcher(message);
+ if (matcher.find()) {
+ String property = matcher.group(1);
+ String value = matcher.group(2);
+
+ // First find the property. We can't just immediately look for the
+ // value, because there could be other attributes in this element
+ // earlier than the one in error, and we might accidentally pick
+ // up on a different occurrence of the value in a context where
+ // it is valid.
+ if (value.length() > 0) {
+ return findRange(file, line, property, value);
+ } else {
+ // Find first occurrence of property followed by '' or ""
+ IRegion region1 = findRange(file, line, property, "\"\""); //$NON-NLS-1$
+ IRegion region2 = findRange(file, line, property, "''"); //$NON-NLS-1$
+ if (region1 == null) {
+ if (region2 == null) {
+ // Highlight the property instead
+ return findRange(file, line, property, null);
+ }
+ return region2;
+ } else if (region2 == null) {
+ return region1;
+ } else if (region1.getOffset() < region2.getOffset()) {
+ return region1;
+ } else {
+ return region2;
+ }
+ }
+ }
+
+ matcher = sRepeatedRangePattern.matcher(message);
+ if (matcher.find()) {
+ String property = matcher.group(2);
+ return findRange(file, line, property, null);
+ }
+
+ matcher = sNoResourcePattern.matcher(message);
+ if (matcher.find()) {
+ String property = matcher.group(1);
+ return findRange(file, line, property, null);
+ }
+
+ matcher = sRequiredPattern.matcher(message);
+ if (matcher.find()) {
+ String elementName = matcher.group(2);
+ IRegion region = findRange(file, line, '<' + elementName, null);
+ if (region != null && region.getLength() > 1) {
+ // Skip the opening <
+ region = new Region(region.getOffset() + 1, region.getLength() - 1);
+ }
+ return region;
+ }
+
+ if (message.endsWith(ORIGINALLY_DEFINED_MSG)) {
+ return findLineTextRange(file, line);
+ }
+
+ return null;
+ }
+
+ /**
+ * Given a file and line number, return the range of the first match starting on the
+ * given line. If second is non null, also search for the second string starting at he
+ * location of the first string.
+ */
+ private static IRegion findRange(IFile file, int line, String first,
+ String second) {
+ IRegion region = null;
+ IDocumentProvider provider = new TextFileDocumentProvider();
+ try {
+ provider.connect(file);
+ IDocument document = provider.getDocument(file);
+ if (document != null) {
+ IRegion lineInfo = document.getLineInformation(line - 1);
+ int lineStartOffset = lineInfo.getOffset();
+ // The aapt errors will be anchored on the line where the
+ // element starts - which means that with formatting where
+ // attributes end up on subsequent lines we don't find it on
+ // the error line indicated by aapt.
+ // Therefore, search forwards in the document.
+ FindReplaceDocumentAdapter adapter =
+ new FindReplaceDocumentAdapter(document);
+
+ region = adapter.find(lineStartOffset, first,
+ true /*forwardSearch*/, true /*caseSensitive*/,
+ false /*wholeWord*/, false /*regExSearch*/);
+ if (region != null && second != null) {
+ region = adapter.find(region.getOffset() + first.length(), second,
+ true /*forwardSearch*/, true /*caseSensitive*/,
+ false /*wholeWord*/, false /*regExSearch*/);
+ }
+ }
+ } catch (Exception e) {
+ AdtPlugin.log(e, "Can't find range information for %1$s", file.getName());
+ } finally {
+ provider.disconnect(file);
+ }
+ return region;
+ }
+
+ /** Returns the non-whitespace line range at the given line number. */
+ private static IRegion findLineTextRange(IFile file, int line) {
+ IDocumentProvider provider = new TextFileDocumentProvider();
+ try {
+ provider.connect(file);
+ IDocument document = provider.getDocument(file);
+ if (document != null) {
+ IRegion lineInfo = document.getLineInformation(line - 1);
+ String lineContents = document.get(lineInfo.getOffset(), lineInfo.getLength());
+ int lineBegin = 0;
+ int lineEnd = lineContents.length()-1;
+
+ for (; lineEnd >= 0; lineEnd--) {
+ char c = lineContents.charAt(lineEnd);
+ if (!Character.isWhitespace(c)) {
+ break;
+ }
+ }
+ lineEnd++;
+ for (; lineBegin < lineEnd; lineBegin++) {
+ char c = lineContents.charAt(lineBegin);
+ if (!Character.isWhitespace(c)) {
+ break;
+ }
+ }
+ if (lineBegin < lineEnd) {
+ return new Region(lineInfo.getOffset() + lineBegin, lineEnd - lineBegin);
+ }
+ }
+ } catch (Exception e) {
+ AdtPlugin.log(e, "Can't find range information for %1$s", file.getName());
+ } finally {
+ provider.disconnect(file);
+ }
+
+ return null;
+ }
+
+ /**
+ * Returns a matching matcher for the next line
+ * @param lines The array of lines
+ * @param nextIndex The index of the next line
+ * @param pattern The pattern to match
+ * @return null if error or no match, the matcher otherwise.
+ */
+ private static final Matcher getNextLineMatcher(String[] lines,
+ int nextIndex, Pattern pattern) {
+ // unless we can't, because we reached the last line
+ if (nextIndex == lines.length) {
+ // we expected a 2nd line, so we flag as error
+ // and we bail
+ return null;
+ }
+
+ Matcher m = pattern.matcher(lines[nextIndex]);
+ if (m.matches()) {
+ return m;
+ }
+
+ return null;
+ }
+
+ private static IResource getResourceFromFullPath(String filename, String root,
+ IProject project) {
+ if (filename.startsWith(root)) {
+ String file = filename.substring(root.length());
+
+ // get the resource
+ IResource r = project.findMember(file);
+
+ // if the resource is valid, we add the marker
+ if (r != null && r.exists()) {
+ return r;
+ }
+ }
+
+ return null;
+ }
+
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/AaptQuickFix.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/AaptQuickFix.java
new file mode 100644
index 000000000..3db380832
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/AaptQuickFix.java
@@ -0,0 +1,431 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.internal.build;
+
+import static com.android.SdkConstants.ANDROID_URI;
+import static com.android.SdkConstants.XMLNS_ANDROID;
+import static com.android.SdkConstants.XMLNS_URI;
+
+import com.android.ide.common.resources.ResourceUrl;
+import com.android.ide.eclipse.adt.AdtConstants;
+import com.android.ide.eclipse.adt.AdtPlugin;
+import com.android.ide.eclipse.adt.AdtUtils;
+import com.android.ide.eclipse.adt.internal.editors.AndroidXmlEditor;
+import com.android.ide.eclipse.adt.internal.resources.ResourceHelper;
+import com.android.resources.ResourceType;
+import com.android.utils.Pair;
+
+import org.eclipse.core.resources.IFile;
+import org.eclipse.core.resources.IMarker;
+import org.eclipse.core.resources.IProject;
+import org.eclipse.core.resources.IResource;
+import org.eclipse.core.runtime.CoreException;
+import org.eclipse.jface.text.BadLocationException;
+import org.eclipse.jface.text.IDocument;
+import org.eclipse.jface.text.IRegion;
+import org.eclipse.jface.text.Region;
+import org.eclipse.jface.text.contentassist.ICompletionProposal;
+import org.eclipse.jface.text.contentassist.IContextInformation;
+import org.eclipse.jface.text.quickassist.IQuickAssistInvocationContext;
+import org.eclipse.jface.text.quickassist.IQuickAssistProcessor;
+import org.eclipse.jface.text.source.Annotation;
+import org.eclipse.jface.text.source.ISourceViewer;
+import org.eclipse.swt.graphics.Image;
+import org.eclipse.swt.graphics.Point;
+import org.eclipse.ui.IMarkerResolution;
+import org.eclipse.ui.IMarkerResolution2;
+import org.eclipse.ui.IMarkerResolutionGenerator2;
+import org.eclipse.ui.PartInitException;
+import org.eclipse.ui.editors.text.TextFileDocumentProvider;
+import org.eclipse.ui.texteditor.IDocumentProvider;
+import org.eclipse.wst.sse.core.StructuredModelManager;
+import org.eclipse.wst.sse.core.internal.provisional.IModelManager;
+import org.eclipse.wst.sse.core.internal.provisional.IStructuredModel;
+import org.eclipse.wst.sse.core.internal.provisional.IndexedRegion;
+import org.eclipse.wst.xml.core.internal.provisional.document.IDOMModel;
+import org.w3c.dom.Attr;
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+
+import java.util.List;
+
+/**
+ * Shared handler for both quick assist processors (Control key handler) and quick fix
+ * marker resolution (Problem view handling), since there is a lot of overlap between
+ * these two UI handlers.
+ */
+@SuppressWarnings("restriction") // XML model
+public class AaptQuickFix implements IMarkerResolutionGenerator2, IQuickAssistProcessor {
+
+ public AaptQuickFix() {
+ }
+
+ /** Returns the error message from aapt that signals missing resources */
+ private static String getTargetMarkerErrorMessage() {
+ return "No resource found that matches the given name";
+ }
+
+ /** Returns the error message from aapt that signals a missing namespace declaration */
+ private static String getUnboundErrorMessage() {
+ return "Error parsing XML: unbound prefix";
+ }
+
+ // ---- Implements IMarkerResolution2 ----
+
+ @Override
+ public boolean hasResolutions(IMarker marker) {
+ String message = null;
+ try {
+ message = (String) marker.getAttribute(IMarker.MESSAGE);
+ } catch (CoreException e) {
+ AdtPlugin.log(e, null);
+ }
+
+ return message != null
+ && (message.contains(getTargetMarkerErrorMessage())
+ || message.contains(getUnboundErrorMessage()));
+ }
+
+ @Override
+ public IMarkerResolution[] getResolutions(IMarker marker) {
+ IResource markerResource = marker.getResource();
+ IProject project = markerResource.getProject();
+ try {
+ String message = (String) marker.getAttribute(IMarker.MESSAGE);
+ if (message.contains(getUnboundErrorMessage()) && markerResource instanceof IFile) {
+ return new IMarkerResolution[] {
+ new CreateNamespaceFix((IFile) markerResource)
+ };
+ }
+ } catch (CoreException e1) {
+ AdtPlugin.log(e1, null);
+ }
+
+ int start = marker.getAttribute(IMarker.CHAR_START, 0);
+ int end = marker.getAttribute(IMarker.CHAR_END, 0);
+ if (end > start) {
+ int length = end - start;
+ IDocumentProvider provider = new TextFileDocumentProvider();
+ try {
+ provider.connect(markerResource);
+ IDocument document = provider.getDocument(markerResource);
+ String resource = document.get(start, length);
+ if (ResourceHelper.canCreateResource(resource)) {
+ return new IMarkerResolution[] {
+ new CreateResourceProposal(project, resource)
+ };
+ }
+ } catch (Exception e) {
+ AdtPlugin.log(e, "Can't find range information for %1$s", markerResource);
+ } finally {
+ provider.disconnect(markerResource);
+ }
+ }
+
+ return null;
+ }
+
+ // ---- Implements IQuickAssistProcessor ----
+
+ @Override
+ public boolean canAssist(IQuickAssistInvocationContext invocationContext) {
+ return true;
+ }
+
+ @Override
+ public boolean canFix(Annotation annotation) {
+ return true;
+ }
+
+ @Override
+ public ICompletionProposal[] computeQuickAssistProposals(
+ IQuickAssistInvocationContext invocationContext) {
+
+ // We have to find the corresponding project/file (so we can look up the aapt
+ // error markers). Unfortunately, an IQuickAssistProcessor only gets
+ // access to an ISourceViewer which has no hooks back to the surrounding
+ // editor.
+ //
+ // However, the IQuickAssistProcessor will only be used interactively by a file
+ // being edited, so we can cheat like the hyperlink detector and simply
+ // look up the currently active file in the IDE. To be on the safe side,
+ // we'll make sure that that editor has the same sourceViewer such that
+ // we are indeed looking at the right file:
+ ISourceViewer sourceViewer = invocationContext.getSourceViewer();
+ AndroidXmlEditor editor = AndroidXmlEditor.fromTextViewer(sourceViewer);
+ if (editor != null) {
+ IFile file = editor.getInputFile();
+ if (file == null) {
+ return null;
+ }
+ IDocument document = sourceViewer.getDocument();
+ List<IMarker> markers = AdtUtils.findMarkersOnLine(AdtConstants.MARKER_AAPT_COMPILE,
+ file, document, invocationContext.getOffset());
+ try {
+ for (IMarker marker : markers) {
+ String message = marker.getAttribute(IMarker.MESSAGE, ""); //$NON-NLS-1$
+ if (message.contains(getTargetMarkerErrorMessage())) {
+ int start = marker.getAttribute(IMarker.CHAR_START, 0);
+ int end = marker.getAttribute(IMarker.CHAR_END, 0);
+ int length = end - start;
+ String resource = document.get(start, length);
+ // Can only offer create value for non-framework value
+ // resources
+ if (ResourceHelper.canCreateResource(resource)) {
+ IProject project = editor.getProject();
+ return new ICompletionProposal[] {
+ new CreateResourceProposal(project, resource)
+ };
+ }
+ } else if (message.contains(getUnboundErrorMessage())) {
+ return new ICompletionProposal[] {
+ new CreateNamespaceFix(null)
+ };
+ }
+ }
+ } catch (BadLocationException e) {
+ AdtPlugin.log(e, null);
+ }
+ }
+
+ return null;
+ }
+
+ @Override
+ public String getErrorMessage() {
+ return null;
+ }
+
+ /** Quick fix to insert namespace binding when missing */
+ private final static class CreateNamespaceFix
+ implements ICompletionProposal, IMarkerResolution2 {
+ private IFile mFile;
+
+ public CreateNamespaceFix(IFile file) {
+ mFile = file;
+ }
+
+ private IndexedRegion perform(IDocument doc) {
+ IModelManager manager = StructuredModelManager.getModelManager();
+ IStructuredModel model = manager.getExistingModelForEdit(doc);
+ if (model != null) {
+ try {
+ perform(model);
+ } finally {
+ model.releaseFromEdit();
+ }
+ }
+
+ return null;
+ }
+
+ private IndexedRegion perform(IFile file) {
+ IModelManager manager = StructuredModelManager.getModelManager();
+ IStructuredModel model;
+ try {
+ model = manager.getModelForEdit(file);
+ if (model != null) {
+ try {
+ perform(model);
+ } finally {
+ model.releaseFromEdit();
+ }
+ }
+ } catch (Exception e) {
+ AdtPlugin.log(e, "Can't look up XML model");
+ }
+
+ return null;
+ }
+
+ private IndexedRegion perform(IStructuredModel model) {
+ if (model instanceof IDOMModel) {
+ IDOMModel domModel = (IDOMModel) model;
+ Document document = domModel.getDocument();
+ Element element = document.getDocumentElement();
+ Attr attr = document.createAttributeNS(XMLNS_URI, XMLNS_ANDROID);
+ attr.setValue(ANDROID_URI);
+ element.getAttributes().setNamedItemNS(attr);
+ return (IndexedRegion) attr;
+ }
+
+ return null;
+ }
+
+ // ---- Implements ICompletionProposal ----
+
+ @Override
+ public void apply(IDocument document) {
+ perform(document);
+ }
+
+ @Override
+ public String getAdditionalProposalInfo() {
+ return "Adds an Android namespace declaratiopn to the root element.";
+ }
+
+ @Override
+ public IContextInformation getContextInformation() {
+ return null;
+ }
+
+ @Override
+ public String getDisplayString() {
+ return "Insert namespace binding";
+ }
+
+ @Override
+ public Image getImage() {
+ return AdtPlugin.getAndroidLogo();
+ }
+
+ @Override
+ public Point getSelection(IDocument doc) {
+ return null;
+ }
+
+
+ // ---- Implements MarkerResolution2 ----
+
+ @Override
+ public String getLabel() {
+ return getDisplayString();
+ }
+
+ @Override
+ public void run(IMarker marker) {
+ try {
+ AdtPlugin.openFile(mFile, null);
+ } catch (PartInitException e) {
+ AdtPlugin.log(e, "Can't open file %1$s", mFile.getName());
+ }
+
+ IndexedRegion indexedRegion = perform(mFile);
+ if (indexedRegion != null) {
+ try {
+ IRegion region =
+ new Region(indexedRegion.getStartOffset(), indexedRegion.getLength());
+ AdtPlugin.openFile(mFile, region);
+ } catch (PartInitException e) {
+ AdtPlugin.log(e, "Can't open file %1$s", mFile.getName());
+ }
+ }
+ }
+
+ @Override
+ public String getDescription() {
+ return getAdditionalProposalInfo();
+ }
+ }
+
+ private static class CreateResourceProposal
+ implements ICompletionProposal, IMarkerResolution2 {
+ private final IProject mProject;
+ private final String mResource;
+
+ CreateResourceProposal(IProject project, String resource) {
+ super();
+ mProject = project;
+ mResource = resource;
+ }
+
+ private void perform() {
+ ResourceUrl resource = ResourceUrl.parse(mResource);
+ if (resource == null) {
+ return;
+ }
+ ResourceType type = resource.type;
+ String name = resource.name;
+ assert !resource.framework;
+ String value = ""; //$NON-NLS-1$
+
+ // Try to pick a reasonable first guess. The new value will be highlighted and
+ // selected for editing, but if we have an initial value then the new file
+ // won't show an error.
+ switch (type) {
+ case STRING: value = "TODO"; break; //$NON-NLS-1$
+ case DIMEN: value = "1dp"; break; //$NON-NLS-1$
+ case BOOL: value = "true"; break; //$NON-NLS-1$
+ case COLOR: value = "#000000"; break; //$NON-NLS-1$
+ case INTEGER: value = "1"; break; //$NON-NLS-1$
+ case ARRAY: value = "<item>1</item>"; break; //$NON-NLS-1$
+ }
+
+ Pair<IFile, IRegion> location =
+ ResourceHelper.createResource(mProject, type, name, value);
+ if (location != null) {
+ IFile file = location.getFirst();
+ IRegion region = location.getSecond();
+ try {
+ AdtPlugin.openFile(file, region);
+ } catch (PartInitException e) {
+ AdtPlugin.log(e, "Can't open file %1$s", file.getName());
+ }
+ }
+ }
+
+ // ---- Implements ICompletionProposal ----
+
+ @Override
+ public void apply(IDocument document) {
+ perform();
+ }
+
+ @Override
+ public String getAdditionalProposalInfo() {
+ return "Creates an XML file entry for the given missing resource "
+ + "and opens it in the editor.";
+ }
+
+ @Override
+ public IContextInformation getContextInformation() {
+ return null;
+ }
+
+ @Override
+ public String getDisplayString() {
+ return String.format("Create resource %1$s", mResource);
+ }
+
+ @Override
+ public Image getImage() {
+ return AdtPlugin.getAndroidLogo();
+ }
+
+ @Override
+ public Point getSelection(IDocument document) {
+ return null;
+ }
+
+ // ---- Implements MarkerResolution2 ----
+
+ @Override
+ public String getLabel() {
+ return getDisplayString();
+ }
+
+ @Override
+ public void run(IMarker marker) {
+ perform();
+ }
+
+ @Override
+ public String getDescription() {
+ return getAdditionalProposalInfo();
+ }
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/AaptResultException.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/AaptResultException.java
new file mode 100644
index 000000000..8d8fe683f
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/AaptResultException.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.internal.build;
+
+/**
+ * Exception thrown when aapt reports an error in the resources.
+ *
+ */
+public final class AaptResultException extends ExecResultException {
+ private static final long serialVersionUID = 1L;
+
+ AaptResultException(int errorCode, String[] output) {
+ super(errorCode, output);
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/AidlProcessor.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/AidlProcessor.java
new file mode 100644
index 000000000..806fa9c40
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/AidlProcessor.java
@@ -0,0 +1,484 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.internal.build;
+
+import com.android.SdkConstants;
+import com.android.annotations.NonNull;
+import com.android.ide.eclipse.adt.AdtConstants;
+import com.android.ide.eclipse.adt.AdtPlugin;
+import com.android.ide.eclipse.adt.internal.build.builders.BaseBuilder;
+import com.android.ide.eclipse.adt.internal.preferences.AdtPrefs;
+import com.android.ide.eclipse.adt.internal.preferences.AdtPrefs.BuildVerbosity;
+import com.android.ide.eclipse.adt.internal.project.BaseProjectHelper;
+import com.android.sdklib.BuildToolInfo;
+import com.android.sdklib.IAndroidTarget;
+import com.android.sdklib.io.FileOp;
+
+import org.eclipse.core.resources.IContainer;
+import org.eclipse.core.resources.IFile;
+import org.eclipse.core.resources.IFolder;
+import org.eclipse.core.resources.IMarker;
+import org.eclipse.core.resources.IProject;
+import org.eclipse.core.resources.IResource;
+import org.eclipse.core.resources.IWorkspaceRoot;
+import org.eclipse.core.resources.ResourcesPlugin;
+import org.eclipse.core.runtime.CoreException;
+import org.eclipse.core.runtime.IPath;
+import org.eclipse.core.runtime.IProgressMonitor;
+import org.eclipse.core.runtime.NullProgressMonitor;
+import org.eclipse.core.runtime.SubProgressMonitor;
+import org.eclipse.jdt.core.IJavaProject;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Set;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * A {@link SourceProcessor} for aidl files.
+ *
+ */
+public class AidlProcessor extends SourceProcessor {
+
+ private static final String PROPERTY_COMPILE_AIDL = "compileAidl"; //$NON-NLS-1$
+
+ /**
+ * Single line aidl error<br>
+ * {@code <path>:<line>: <error>}<br>
+ * or<br>
+ * {@code <path>:<line> <error>}<br>
+ */
+ private static Pattern sAidlPattern1 = Pattern.compile("^(.+?):(\\d+):?\\s(.+)$"); //$NON-NLS-1$
+
+ private final static Set<String> EXTENSIONS = Collections.singleton(SdkConstants.EXT_AIDL);
+
+ private enum AidlType {
+ UNKNOWN, INTERFACE, PARCELABLE;
+ }
+
+ // See comment in #getAidlType()
+// private final static Pattern sParcelablePattern = Pattern.compile(
+// "^\\s*parcelable\\s+([a-zA-Z_][a-zA-Z0-9_]*)\\s*;\\s*$");
+//
+// private final static Pattern sInterfacePattern = Pattern.compile(
+// "^\\s*interface\\s+([a-zA-Z_][a-zA-Z0-9_]*)\\s*(?:\\{.*)?$");
+
+
+ public AidlProcessor(@NonNull IJavaProject javaProject, @NonNull BuildToolInfo buildToolInfo,
+ @NonNull IFolder genFolder) {
+ super(javaProject, buildToolInfo, genFolder);
+ }
+
+ @Override
+ protected Set<String> getExtensions() {
+ return EXTENSIONS;
+ }
+
+ @Override
+ protected String getSavePropertyName() {
+ return PROPERTY_COMPILE_AIDL;
+ }
+
+ @Override
+ protected void doCompileFiles(List<IFile> sources, BaseBuilder builder,
+ IProject project, IAndroidTarget projectTarget,
+ List<IPath> sourceFolders, List<IFile> notCompiledOut, List<File> libraryProjectsOut,
+ IProgressMonitor monitor) throws CoreException {
+ // create the command line
+ List<String> commandList = new ArrayList<String>(
+ 4 + sourceFolders.size() + libraryProjectsOut.size());
+ commandList.add(getBuildToolInfo().getPath(BuildToolInfo.PathId.AIDL));
+ commandList.add(quote("-p" + projectTarget.getPath(IAndroidTarget.ANDROID_AIDL))); //$NON-NLS-1$
+
+ // since the path are relative to the workspace and not the project itself, we need
+ // the workspace root.
+ IWorkspaceRoot wsRoot = ResourcesPlugin.getWorkspace().getRoot();
+ for (IPath p : sourceFolders) {
+ IFolder f = wsRoot.getFolder(p);
+ if (f.exists()) { // if the resource doesn't exist, getLocation will return null.
+ commandList.add(quote("-I" + f.getLocation().toOSString())); //$NON-NLS-1$
+ }
+ }
+
+ for (File libOut : libraryProjectsOut) {
+ // FIXME: make folder configurable
+ File aidlFile = new File(libOut, SdkConstants.FD_AIDL);
+ if (aidlFile.isDirectory()) {
+ commandList.add(quote("-I" + aidlFile.getAbsolutePath())); //$NON-NLS-1$
+ }
+ }
+
+ // convert to array with 2 extra strings for the in/out file paths.
+ int index = commandList.size();
+ String[] commands = commandList.toArray(new String[index + 2]);
+
+ boolean verbose = AdtPrefs.getPrefs().getBuildVerbosity() == BuildVerbosity.VERBOSE;
+
+ // remove the generic marker from the project
+ builder.removeMarkersFromResource(project, AdtConstants.MARKER_AIDL);
+
+ // prepare the two output folders.
+ IFolder genFolder = getGenFolder();
+ IFolder projectOut = BaseProjectHelper.getAndroidOutputFolder(project);
+ IFolder aidlOutFolder = projectOut.getFolder(SdkConstants.FD_AIDL);
+ if (aidlOutFolder.exists() == false) {
+ aidlOutFolder.create(true /*force*/, true /*local*/,
+ new SubProgressMonitor(monitor, 10));
+ }
+
+ boolean success = false;
+
+ // loop until we've compile them all
+ for (IFile sourceFile : sources) {
+ if (verbose) {
+ String name = sourceFile.getName();
+ IPath sourceFolderPath = getSourceFolderFor(sourceFile);
+ if (sourceFolderPath != null) {
+ // make a path to the source file relative to the source folder.
+ IPath relative = sourceFile.getFullPath().makeRelativeTo(sourceFolderPath);
+ name = relative.toString();
+ }
+ AdtPlugin.printToConsole(project, "AIDL: " + name);
+ }
+
+ // Remove the AIDL error markers from the aidl file
+ builder.removeMarkersFromResource(sourceFile, AdtConstants.MARKER_AIDL);
+
+ // get the path of the source file.
+ IPath sourcePath = sourceFile.getLocation();
+ String osSourcePath = sourcePath.toOSString();
+
+ // look if we already know the output
+ SourceFileData data = getFileData(sourceFile);
+ if (data == null) {
+ data = new SourceFileData(sourceFile);
+ addData(data);
+ }
+
+ // if there's no output file yet, compute it.
+ if (data.getOutput() == null) {
+ IFile javaFile = getAidlOutputFile(sourceFile, genFolder,
+ true /*replaceExt*/, true /*createFolders*/, monitor);
+ data.setOutputFile(javaFile);
+ }
+
+ // finish to set the command line.
+ commands[index] = quote(osSourcePath);
+ commands[index + 1] = quote(data.getOutput().getLocation().toOSString());
+
+ // launch the process
+ if (execAidl(builder, project, commands, sourceFile, verbose) == false) {
+ // aidl failed. File should be marked. We add the file to the list
+ // of file that will need compilation again.
+ notCompiledOut.add(sourceFile);
+ } else {
+ // Success. we'll return that we generated code
+ setCompilationStatus(COMPILE_STATUS_CODE);
+ success = true;
+
+ // Also copy the file to the bin folder.
+ IFile aidlOutFile = getAidlOutputFile(sourceFile, aidlOutFolder,
+ false /*replaceExt*/, true /*createFolders*/, monitor);
+
+ FileOp op = new FileOp();
+ try {
+ op.copyFile(sourceFile.getLocation().toFile(),
+ aidlOutFile.getLocation().toFile());
+ } catch (IOException e) {
+ }
+ }
+ }
+
+ if (success) {
+ aidlOutFolder.refreshLocal(IResource.DEPTH_INFINITE, monitor);
+ }
+ }
+
+ @Override
+ protected void loadOutputAndDependencies() {
+ IProgressMonitor monitor = new NullProgressMonitor();
+ IFolder genFolder = getGenFolder();
+
+ Collection<SourceFileData> dataList = getAllFileData();
+ for (SourceFileData data : dataList) {
+ try {
+ IFile javaFile = getAidlOutputFile(data.getSourceFile(), genFolder,
+ true /*replaceExt*/, false /*createFolders*/, monitor);
+ data.setOutputFile(javaFile);
+ } catch (CoreException e) {
+ // ignore, we're not asking to create the folder so this won't happen anyway.
+ }
+
+ }
+ }
+
+ /**
+ * Execute the aidl command line, parse the output, and mark the aidl file
+ * with any reported errors.
+ * @param command the String array containing the command line to execute.
+ * @param file The IFile object representing the aidl file being
+ * compiled.
+ * @param verbose the build verbosity
+ * @return false if the exec failed, and build needs to be aborted.
+ */
+ private boolean execAidl(BaseBuilder builder, IProject project, String[] command, IFile file,
+ boolean verbose) {
+ // do the exec
+ try {
+ if (verbose) {
+ StringBuilder sb = new StringBuilder();
+ for (String c : command) {
+ sb.append(c);
+ sb.append(' ');
+ }
+ String cmd_line = sb.toString();
+ AdtPlugin.printToConsole(project, cmd_line);
+ }
+
+ Process p = Runtime.getRuntime().exec(command);
+
+ // list to store each line of stderr
+ ArrayList<String> stdErr = new ArrayList<String>();
+
+ // get the output and return code from the process
+ int returnCode = BuildHelper.grabProcessOutput(project, p, stdErr);
+
+ if (stdErr.size() > 0) {
+ // attempt to parse the error output
+ boolean parsingError = parseAidlOutput(stdErr, file);
+
+ // If the process failed and we couldn't parse the output
+ // we print a message, mark the project and exit
+ if (returnCode != 0) {
+
+ if (parsingError || verbose) {
+ // display the message in the console.
+ if (parsingError) {
+ AdtPlugin.printErrorToConsole(project, stdErr.toArray());
+
+ // mark the project
+ BaseProjectHelper.markResource(project, AdtConstants.MARKER_AIDL,
+ Messages.Unparsed_AIDL_Errors, IMarker.SEVERITY_ERROR);
+ } else {
+ AdtPlugin.printToConsole(project, stdErr.toArray());
+ }
+ }
+ return false;
+ }
+ } else if (returnCode != 0) {
+ // no stderr output but exec failed.
+ String msg = String.format(Messages.AIDL_Exec_Error_d, returnCode);
+
+ BaseProjectHelper.markResource(project, AdtConstants.MARKER_AIDL,
+ msg, IMarker.SEVERITY_ERROR);
+
+ return false;
+ }
+ } catch (IOException e) {
+ // mark the project and exit
+ String msg = String.format(Messages.AIDL_Exec_Error_s, command[0]);
+ BaseProjectHelper.markResource(project, AdtConstants.MARKER_AIDL, msg,
+ IMarker.SEVERITY_ERROR);
+ return false;
+ } catch (InterruptedException e) {
+ // mark the project and exit
+ String msg = String.format(Messages.AIDL_Exec_Error_s, command[0]);
+ BaseProjectHelper.markResource(project, AdtConstants.MARKER_AIDL, msg,
+ IMarker.SEVERITY_ERROR);
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Parse the output of aidl and mark the file with any errors.
+ * @param lines The output to parse.
+ * @param file The file to mark with error.
+ * @return true if the parsing failed, false if success.
+ */
+ private boolean parseAidlOutput(ArrayList<String> lines, IFile file) {
+ // nothing to parse? just return false;
+ if (lines.size() == 0) {
+ return false;
+ }
+
+ Matcher m;
+
+ for (int i = 0; i < lines.size(); i++) {
+ String p = lines.get(i);
+
+ m = sAidlPattern1.matcher(p);
+ if (m.matches()) {
+ // we can ignore group 1 which is the location since we already
+ // have a IFile object representing the aidl file.
+ String lineStr = m.group(2);
+ String msg = m.group(3);
+
+ // get the line number
+ int line = 0;
+ try {
+ line = Integer.parseInt(lineStr);
+ } catch (NumberFormatException e) {
+ // looks like the string we extracted wasn't a valid
+ // file number. Parsing failed and we return true
+ return true;
+ }
+
+ // mark the file
+ BaseProjectHelper.markResource(file, AdtConstants.MARKER_AIDL, msg, line,
+ IMarker.SEVERITY_ERROR);
+
+ // success, go to the next line
+ continue;
+ }
+
+ // invalid line format, flag as error, and bail
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Returns the {@link IFile} handle to the destination file for a given aidl source file
+ * ({@link AidlData}).
+ * @param sourceFile The source file
+ * @param outputFolder the top level output folder (not including the package folders)
+ * @param createFolders whether or not the parent folder of the destination should be created
+ * if it does not exist.
+ * @param monitor the progress monitor
+ * @return the handle to the destination file.
+ * @throws CoreException
+ */
+ private IFile getAidlOutputFile(IFile sourceFile, IFolder outputFolder, boolean replaceExt,
+ boolean createFolders, IProgressMonitor monitor) throws CoreException {
+
+ IPath sourceFolderPath = getSourceFolderFor(sourceFile);
+
+ // this really shouldn't happen since the sourceFile must be in a source folder
+ // since it comes from the delta visitor
+ if (sourceFolderPath != null) {
+ // make a path to the source file relative to the source folder.
+ IPath relative = sourceFile.getFullPath().makeRelativeTo(sourceFolderPath);
+ // remove the file name. This is now the destination folder.
+ relative = relative.removeLastSegments(1);
+
+ // get an IFolder for this path.
+ IFolder destinationFolder = outputFolder.getFolder(relative);
+
+ // create it if needed.
+ if (destinationFolder.exists() == false && createFolders) {
+ createFolder(destinationFolder, monitor);
+ }
+
+ // Build the Java file name from the aidl name.
+ String javaName;
+ if (replaceExt) {
+ javaName = sourceFile.getName().replaceAll(
+ AdtConstants.RE_AIDL_EXT, SdkConstants.DOT_JAVA);
+ } else {
+ javaName = sourceFile.getName();
+ }
+
+ // get the resource for the java file.
+ IFile javaFile = destinationFolder.getFile(javaName);
+ return javaFile;
+ }
+
+ return null;
+ }
+
+ /**
+ * Creates the destination folder. Because
+ * {@link IFolder#create(boolean, boolean, IProgressMonitor)} only works if the parent folder
+ * already exists, this goes and ensure that all the parent folders actually exist, or it
+ * creates them as well.
+ * @param destinationFolder The folder to create
+ * @param monitor the {@link IProgressMonitor},
+ * @throws CoreException
+ */
+ private void createFolder(IFolder destinationFolder, IProgressMonitor monitor)
+ throws CoreException {
+
+ // check the parent exist and create if necessary.
+ IContainer parent = destinationFolder.getParent();
+ if (parent.getType() == IResource.FOLDER && parent.exists() == false) {
+ createFolder((IFolder)parent, monitor);
+ }
+
+ // create the folder.
+ destinationFolder.create(true /*force*/, true /*local*/,
+ new SubProgressMonitor(monitor, 10));
+ }
+
+ /**
+ * Returns the type of the aidl file. Aidl files can either declare interfaces, or declare
+ * parcelables. This method will attempt to parse the file and return the type. If the type
+ * cannot be determined, then it will return {@link AidlType#UNKNOWN}.
+ * @param file The aidl file
+ * @return the type of the aidl.
+ */
+ private static AidlType getAidlType(IFile file) {
+ // At this time, parsing isn't available, so we return UNKNOWN. This will force
+ // a recompilation of all aidl file as soon as one is changed.
+ return AidlType.UNKNOWN;
+
+ // TODO: properly parse aidl file to determine type and generate dependency graphs.
+//
+// String className = file.getName().substring(0,
+// file.getName().length() - SdkConstants.DOT_AIDL.length());
+//
+// InputStream input = file.getContents(true /* force*/);
+// try {
+// BufferedReader reader = new BufferedReader(new InputStreateader(input));
+// String line;
+// while ((line = reader.readLine()) != null) {
+// if (line.length() == 0) {
+// continue;
+// }
+//
+// Matcher m = sParcelablePattern.matcher(line);
+// if (m.matches() && m.group(1).equals(className)) {
+// return AidlType.PARCELABLE;
+// }
+//
+// m = sInterfacePattern.matcher(line);
+// if (m.matches() && m.group(1).equals(className)) {
+// return AidlType.INTERFACE;
+// }
+// }
+// } catch (IOException e) {
+// throw new CoreException(new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID,
+// "Error parsing aidl file", e));
+// } finally {
+// try {
+// input.close();
+// } catch (IOException e) {
+// throw new CoreException(new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID,
+// "Error parsing aidl file", e));
+// }
+// }
+//
+// return AidlType.UNKNOWN;
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/BuildHelper.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/BuildHelper.java
new file mode 100644
index 000000000..78d9d94e4
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/BuildHelper.java
@@ -0,0 +1,1225 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.internal.build;
+
+import com.android.SdkConstants;
+import com.android.annotations.NonNull;
+import com.android.annotations.Nullable;
+import com.android.ide.eclipse.adt.AdtConstants;
+import com.android.ide.eclipse.adt.AdtPlugin;
+import com.android.ide.eclipse.adt.AndroidPrintStream;
+import com.android.ide.eclipse.adt.internal.preferences.AdtPrefs;
+import com.android.ide.eclipse.adt.internal.preferences.AdtPrefs.BuildVerbosity;
+import com.android.ide.eclipse.adt.internal.project.BaseProjectHelper;
+import com.android.ide.eclipse.adt.internal.sdk.ProjectState;
+import com.android.ide.eclipse.adt.internal.sdk.Sdk;
+import com.android.prefs.AndroidLocation.AndroidLocationException;
+import com.android.sdklib.BuildToolInfo;
+import com.android.sdklib.IAndroidTarget;
+import com.android.sdklib.IAndroidTarget.IOptionalLibrary;
+import com.android.sdklib.build.ApkBuilder;
+import com.android.sdklib.build.ApkBuilder.JarStatus;
+import com.android.sdklib.build.ApkBuilder.SigningInfo;
+import com.android.sdklib.build.ApkCreationException;
+import com.android.sdklib.build.DuplicateFileException;
+import com.android.sdklib.build.RenderScriptProcessor;
+import com.android.sdklib.build.SealedApkException;
+import com.android.sdklib.internal.build.DebugKeyProvider;
+import com.android.sdklib.internal.build.DebugKeyProvider.KeytoolException;
+import com.android.utils.GrabProcessOutput;
+import com.android.utils.GrabProcessOutput.IProcessOutput;
+import com.android.utils.GrabProcessOutput.Wait;
+import com.google.common.base.Charsets;
+import com.google.common.hash.HashCode;
+import com.google.common.hash.HashFunction;
+import com.google.common.hash.Hashing;
+
+import org.eclipse.core.resources.IFile;
+import org.eclipse.core.resources.IFolder;
+import org.eclipse.core.resources.IProject;
+import org.eclipse.core.resources.IResource;
+import org.eclipse.core.resources.IWorkspaceRoot;
+import org.eclipse.core.resources.ResourcesPlugin;
+import org.eclipse.core.runtime.CoreException;
+import org.eclipse.core.runtime.IPath;
+import org.eclipse.core.runtime.IStatus;
+import org.eclipse.core.runtime.Status;
+import org.eclipse.jdt.core.IClasspathContainer;
+import org.eclipse.jdt.core.IClasspathEntry;
+import org.eclipse.jdt.core.IJavaProject;
+import org.eclipse.jdt.core.JavaCore;
+import org.eclipse.jdt.core.JavaModelException;
+import org.eclipse.jface.preference.IPreferenceStore;
+
+import java.io.File;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.io.PrintStream;
+import java.security.PrivateKey;
+import java.security.cert.X509Certificate;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.TreeMap;
+
+/**
+ * Helper with methods for the last 3 steps of the generation of an APK.
+ *
+ * {@link #packageResources(IFile, IProject[], String, int, String, String)} packages the
+ * application resources using aapt into a zip file that is ready to be integrated into the apk.
+ *
+ * {@link #executeDx(IJavaProject, String, String, IJavaProject[])} will convert the Java byte
+ * code into the Dalvik bytecode.
+ *
+ * {@link #finalPackage(String, String, String, boolean, IJavaProject, IProject[], IJavaProject[], String, boolean)}
+ * will make the apk from all the previous components.
+ *
+ * This class only executes the 3 above actions. It does not handle the errors, and simply sends
+ * them back as custom exceptions.
+ *
+ * Warnings are handled by the {@link ResourceMarker} interface.
+ *
+ * Console output (verbose and non verbose) is handled through the {@link AndroidPrintStream} passed
+ * to the constructor.
+ *
+ */
+public class BuildHelper {
+
+ private static final String CONSOLE_PREFIX_DX = "Dx"; //$NON-NLS-1$
+ private final static String TEMP_PREFIX = "android_"; //$NON-NLS-1$
+
+ private static final String COMMAND_CRUNCH = "crunch"; //$NON-NLS-1$
+ private static final String COMMAND_PACKAGE = "package"; //$NON-NLS-1$
+
+ @NonNull
+ private final ProjectState mProjectState;
+ @NonNull
+ private final IProject mProject;
+ @NonNull
+ private final BuildToolInfo mBuildToolInfo;
+ @NonNull
+ private final AndroidPrintStream mOutStream;
+ @NonNull
+ private final AndroidPrintStream mErrStream;
+ private final boolean mForceJumbo;
+ private final boolean mDisableDexMerger;
+ private final boolean mVerbose;
+ private final boolean mDebugMode;
+
+ private final Set<String> mCompiledCodePaths = new HashSet<String>();
+
+ public static final boolean BENCHMARK_FLAG = false;
+ public static long sStartOverallTime = 0;
+ public static long sStartJavaCTime = 0;
+
+ private final static int MILLION = 1000000;
+ private String mProguardFile;
+
+ /**
+ * An object able to put a marker on a resource.
+ */
+ public interface ResourceMarker {
+ void setWarning(IResource resource, String message);
+ }
+
+ /**
+ * Creates a new post-compiler helper
+ * @param project
+ * @param outStream
+ * @param errStream
+ * @param debugMode whether this is a debug build
+ * @param verbose
+ * @throws CoreException
+ */
+ public BuildHelper(@NonNull ProjectState projectState,
+ @NonNull BuildToolInfo buildToolInfo,
+ @NonNull AndroidPrintStream outStream,
+ @NonNull AndroidPrintStream errStream,
+ boolean forceJumbo, boolean disableDexMerger, boolean debugMode,
+ boolean verbose, ResourceMarker resMarker) throws CoreException {
+ mProjectState = projectState;
+ mProject = projectState.getProject();
+ mBuildToolInfo = buildToolInfo;
+ mOutStream = outStream;
+ mErrStream = errStream;
+ mDebugMode = debugMode;
+ mVerbose = verbose;
+ mForceJumbo = forceJumbo;
+ mDisableDexMerger = disableDexMerger;
+
+ gatherPaths(resMarker);
+ }
+
+ public void updateCrunchCache() throws AaptExecException, AaptResultException {
+ // Benchmarking start
+ long startCrunchTime = 0;
+ if (BENCHMARK_FLAG) {
+ String msg = "BENCHMARK ADT: Starting Initial Packaging (.ap_)"; //$NON-NLS-1$
+ AdtPlugin.printBuildToConsole(BuildVerbosity.ALWAYS, mProject, msg);
+ startCrunchTime = System.nanoTime();
+ }
+
+ // Get the resources folder to crunch from
+ IFolder resFolder = mProject.getFolder(AdtConstants.WS_RESOURCES);
+ List<String> resPaths = new ArrayList<String>();
+ resPaths.add(resFolder.getLocation().toOSString());
+
+ // Get the output folder where the cache is stored.
+ IFolder binFolder = BaseProjectHelper.getAndroidOutputFolder(mProject);
+ IFolder cacheFolder = binFolder.getFolder(AdtConstants.WS_BIN_RELATIVE_CRUNCHCACHE);
+ String cachePath = cacheFolder.getLocation().toOSString();
+
+ /* For crunching, we don't need the osManifestPath, osAssetsPath, or the configFilter
+ * parameters for executeAapt
+ */
+ executeAapt(COMMAND_CRUNCH, "", resPaths, "", cachePath, "", 0);
+
+ // Benchmarking end
+ if (BENCHMARK_FLAG) {
+ String msg = "BENCHMARK ADT: Ending Initial Package (.ap_). \nTime Elapsed: " //$NON-NLS-1$
+ + ((System.nanoTime() - startCrunchTime)/MILLION) + "ms"; //$NON-NLS-1$
+ AdtPlugin.printBuildToConsole(BuildVerbosity.ALWAYS, mProject, msg);
+ }
+ }
+
+ /**
+ * Packages the resources of the projet into a .ap_ file.
+ * @param manifestFile the manifest of the project.
+ * @param libProjects the list of library projects that this project depends on.
+ * @param resFilter an optional resource filter to be used with the -c option of aapt. If null
+ * no filters are used.
+ * @param versionCode an optional versionCode to be inserted in the manifest during packaging.
+ * If the value is <=0, no values are inserted.
+ * @param outputFolder where to write the resource ap_ file.
+ * @param outputFilename the name of the resource ap_ file.
+ * @throws AaptExecException
+ * @throws AaptResultException
+ */
+ public void packageResources(IFile manifestFile, List<IProject> libProjects, String resFilter,
+ int versionCode, String outputFolder, String outputFilename)
+ throws AaptExecException, AaptResultException {
+
+ // Benchmarking start
+ long startPackageTime = 0;
+ if (BENCHMARK_FLAG) {
+ String msg = "BENCHMARK ADT: Starting Initial Packaging (.ap_)"; //$NON-NLS-1$
+ AdtPlugin.printBuildToConsole(BuildVerbosity.ALWAYS, mProject, msg);
+ startPackageTime = System.nanoTime();
+ }
+
+ // need to figure out some path before we can execute aapt;
+ IFolder binFolder = BaseProjectHelper.getAndroidOutputFolder(mProject);
+
+ // get the cache folder
+ IFolder cacheFolder = binFolder.getFolder(AdtConstants.WS_BIN_RELATIVE_CRUNCHCACHE);
+
+ // get the BC folder
+ IFolder bcFolder = binFolder.getFolder(AdtConstants.WS_BIN_RELATIVE_BC);
+
+ // get the resource folder
+ IFolder resFolder = mProject.getFolder(AdtConstants.WS_RESOURCES);
+
+ // and the assets folder
+ IFolder assetsFolder = mProject.getFolder(AdtConstants.WS_ASSETS);
+
+ // we need to make sure this one exists.
+ if (assetsFolder.exists() == false) {
+ assetsFolder = null;
+ }
+
+ // list of res folder (main project + maybe libraries)
+ ArrayList<String> osResPaths = new ArrayList<String>();
+
+ IPath resLocation = resFolder.getLocation();
+ IPath manifestLocation = manifestFile.getLocation();
+
+ if (resLocation != null && manifestLocation != null) {
+
+ // png cache folder first.
+ addFolderToList(osResPaths, cacheFolder);
+ addFolderToList(osResPaths, bcFolder);
+
+ // regular res folder next.
+ osResPaths.add(resLocation.toOSString());
+
+ // then libraries
+ if (libProjects != null) {
+ for (IProject lib : libProjects) {
+ // png cache folder first
+ IFolder libBinFolder = BaseProjectHelper.getAndroidOutputFolder(lib);
+
+ IFolder libCacheFolder = libBinFolder.getFolder(AdtConstants.WS_BIN_RELATIVE_CRUNCHCACHE);
+ addFolderToList(osResPaths, libCacheFolder);
+
+ IFolder libBcFolder = libBinFolder.getFolder(AdtConstants.WS_BIN_RELATIVE_BC);
+ addFolderToList(osResPaths, libBcFolder);
+
+ // regular res folder next.
+ IFolder libResFolder = lib.getFolder(AdtConstants.WS_RESOURCES);
+ addFolderToList(osResPaths, libResFolder);
+ }
+ }
+
+ String osManifestPath = manifestLocation.toOSString();
+
+ String osAssetsPath = null;
+ if (assetsFolder != null) {
+ osAssetsPath = assetsFolder.getLocation().toOSString();
+ }
+
+ // build the default resource package
+ executeAapt(COMMAND_PACKAGE, osManifestPath, osResPaths, osAssetsPath,
+ outputFolder + File.separator + outputFilename, resFilter,
+ versionCode);
+ }
+
+ // Benchmarking end
+ if (BENCHMARK_FLAG) {
+ String msg = "BENCHMARK ADT: Ending Initial Package (.ap_). \nTime Elapsed: " //$NON-NLS-1$
+ + ((System.nanoTime() - startPackageTime)/MILLION) + "ms"; //$NON-NLS-1$
+ AdtPlugin.printBuildToConsole(BuildVerbosity.ALWAYS, mProject, msg);
+ }
+ }
+
+ /**
+ * Adds os path of a folder to a list only if the folder actually exists.
+ * @param pathList
+ * @param folder
+ */
+ private void addFolderToList(List<String> pathList, IFolder folder) {
+ // use a File instead of the IFolder API to ignore workspace refresh issue.
+ File testFile = new File(folder.getLocation().toOSString());
+ if (testFile.isDirectory()) {
+ pathList.add(testFile.getAbsolutePath());
+ }
+ }
+
+ /**
+ * Makes a final package signed with the debug key.
+ *
+ * Packages the dex files, the temporary resource file into the final package file.
+ *
+ * Whether the package is a debug package is controlled with the <var>debugMode</var> parameter
+ * in {@link #PostCompilerHelper(IProject, PrintStream, PrintStream, boolean, boolean)}
+ *
+ * @param intermediateApk The path to the temporary resource file.
+ * @param dex The path to the dex file.
+ * @param output The path to the final package file to create.
+ * @param libProjects an optional list of library projects (can be null)
+ * @return true if success, false otherwise.
+ * @throws ApkCreationException
+ * @throws AndroidLocationException
+ * @throws KeytoolException
+ * @throws NativeLibInJarException
+ * @throws CoreException
+ * @throws DuplicateFileException
+ */
+ public void finalDebugPackage(String intermediateApk, String dex, String output,
+ List<IProject> libProjects, ResourceMarker resMarker)
+ throws ApkCreationException, KeytoolException, AndroidLocationException,
+ NativeLibInJarException, DuplicateFileException, CoreException {
+
+ AdtPlugin adt = AdtPlugin.getDefault();
+ if (adt == null) {
+ return;
+ }
+
+ // get the debug keystore to use.
+ IPreferenceStore store = adt.getPreferenceStore();
+ String keystoreOsPath = store.getString(AdtPrefs.PREFS_CUSTOM_DEBUG_KEYSTORE);
+ if (keystoreOsPath == null || new File(keystoreOsPath).isFile() == false) {
+ keystoreOsPath = DebugKeyProvider.getDefaultKeyStoreOsPath();
+ AdtPlugin.printBuildToConsole(BuildVerbosity.VERBOSE, mProject,
+ Messages.ApkBuilder_Using_Default_Key);
+ } else {
+ AdtPlugin.printBuildToConsole(BuildVerbosity.VERBOSE, mProject,
+ String.format(Messages.ApkBuilder_Using_s_To_Sign, keystoreOsPath));
+ }
+
+ // from the keystore, get the signing info
+ SigningInfo info = ApkBuilder.getDebugKey(keystoreOsPath, mVerbose ? mOutStream : null);
+
+ finalPackage(intermediateApk, dex, output, libProjects,
+ info != null ? info.key : null, info != null ? info.certificate : null, resMarker);
+ }
+
+ /**
+ * Makes the final package.
+ *
+ * Packages the dex files, the temporary resource file into the final package file.
+ *
+ * Whether the package is a debug package is controlled with the <var>debugMode</var> parameter
+ * in {@link #PostCompilerHelper(IProject, PrintStream, PrintStream, boolean, boolean)}
+ *
+ * @param intermediateApk The path to the temporary resource file.
+ * @param dex The path to the dex file.
+ * @param output The path to the final package file to create.
+ * @param debugSign whether the apk must be signed with the debug key.
+ * @param libProjects an optional list of library projects (can be null)
+ * @param abiFilter an optional filter. If not null, then only the matching ABI is included in
+ * the final archive
+ * @return true if success, false otherwise.
+ * @throws NativeLibInJarException
+ * @throws ApkCreationException
+ * @throws CoreException
+ * @throws DuplicateFileException
+ */
+ public void finalPackage(String intermediateApk, String dex, String output,
+ List<IProject> libProjects,
+ PrivateKey key, X509Certificate certificate, ResourceMarker resMarker)
+ throws NativeLibInJarException, ApkCreationException, DuplicateFileException,
+ CoreException {
+
+ try {
+ ApkBuilder apkBuilder = new ApkBuilder(output, intermediateApk, dex,
+ key, certificate,
+ mVerbose ? mOutStream: null);
+ apkBuilder.setDebugMode(mDebugMode);
+
+ // either use the full compiled code paths or just the proguard file
+ // if present
+ Collection<String> pathsCollection = mCompiledCodePaths;
+ if (mProguardFile != null) {
+ pathsCollection = Collections.singletonList(mProguardFile);
+ mProguardFile = null;
+ }
+
+ // Now we write the standard resources from all the output paths.
+ for (String path : pathsCollection) {
+ File file = new File(path);
+ if (file.isFile()) {
+ JarStatus jarStatus = apkBuilder.addResourcesFromJar(file);
+
+ // check if we found native libraries in the external library. This
+ // constitutes an error or warning depending on if they are in lib/
+ if (jarStatus.getNativeLibs().size() > 0) {
+ String libName = file.getName();
+
+ String msg = String.format(
+ "Native libraries detected in '%1$s'. See console for more information.",
+ libName);
+
+ ArrayList<String> consoleMsgs = new ArrayList<String>();
+
+ consoleMsgs.add(String.format(
+ "The library '%1$s' contains native libraries that will not run on the device.",
+ libName));
+
+ if (jarStatus.hasNativeLibsConflicts()) {
+ consoleMsgs.add("Additionally some of those libraries will interfer with the installation of the application because of their location in lib/");
+ consoleMsgs.add("lib/ is reserved for NDK libraries.");
+ }
+
+ consoleMsgs.add("The following libraries were found:");
+
+ for (String lib : jarStatus.getNativeLibs()) {
+ consoleMsgs.add(" - " + lib);
+ }
+
+ String[] consoleStrings = consoleMsgs.toArray(new String[consoleMsgs.size()]);
+
+ // if there's a conflict or if the prefs force error on any native code in jar
+ // files, throw an exception
+ if (jarStatus.hasNativeLibsConflicts() ||
+ AdtPrefs.getPrefs().getBuildForceErrorOnNativeLibInJar()) {
+ throw new NativeLibInJarException(jarStatus, msg, libName, consoleStrings);
+ } else {
+ // otherwise, put a warning, and output to the console also.
+ if (resMarker != null) {
+ resMarker.setWarning(mProject, msg);
+ }
+
+ for (String string : consoleStrings) {
+ mOutStream.println(string);
+ }
+ }
+ }
+ } else if (file.isDirectory()) {
+ // this is technically not a source folder (class folder instead) but since we
+ // only care about Java resources (ie non class/java files) this will do the
+ // same
+ apkBuilder.addSourceFolder(file);
+ }
+ }
+
+ // now write the native libraries.
+ // First look if the lib folder is there.
+ IResource libFolder = mProject.findMember(SdkConstants.FD_NATIVE_LIBS);
+ if (libFolder != null && libFolder.exists() &&
+ libFolder.getType() == IResource.FOLDER) {
+ // get a File for the folder.
+ apkBuilder.addNativeLibraries(libFolder.getLocation().toFile());
+ }
+
+ // next the native libraries for the renderscript support mode.
+ if (mProjectState.getRenderScriptSupportMode()) {
+ IFolder androidOutputFolder = BaseProjectHelper.getAndroidOutputFolder(mProject);
+ IResource rsLibFolder = androidOutputFolder.getFolder(
+ AdtConstants.WS_BIN_RELATIVE_RS_LIBS);
+ File rsLibFolderFile = rsLibFolder.getLocation().toFile();
+ if (rsLibFolderFile.isDirectory()) {
+ apkBuilder.addNativeLibraries(rsLibFolderFile);
+ }
+
+ File rsLibs = RenderScriptProcessor.getSupportNativeLibFolder(
+ mBuildToolInfo.getLocation().getAbsolutePath());
+ if (rsLibs.isDirectory()) {
+ apkBuilder.addNativeLibraries(rsLibs);
+ }
+ }
+
+ // write the native libraries for the library projects.
+ if (libProjects != null) {
+ for (IProject lib : libProjects) {
+ libFolder = lib.findMember(SdkConstants.FD_NATIVE_LIBS);
+ if (libFolder != null && libFolder.exists() &&
+ libFolder.getType() == IResource.FOLDER) {
+ apkBuilder.addNativeLibraries(libFolder.getLocation().toFile());
+ }
+ }
+ }
+
+ // seal the APK.
+ apkBuilder.sealApk();
+ } catch (SealedApkException e) {
+ // this won't happen as we control when the apk is sealed.
+ }
+ }
+
+ public void setProguardOutput(String proguardFile) {
+ mProguardFile = proguardFile;
+ }
+
+ public Collection<String> getCompiledCodePaths() {
+ return mCompiledCodePaths;
+ }
+
+ public void runProguard(List<File> proguardConfigs, File inputJar, Collection<String> jarFiles,
+ File obfuscatedJar, File logOutput)
+ throws ProguardResultException, ProguardExecException, IOException {
+ IAndroidTarget target = Sdk.getCurrent().getTarget(mProject);
+
+ // prepare the command line for proguard
+ List<String> command = new ArrayList<String>();
+ command.add(AdtPlugin.getOsAbsoluteProguard());
+
+ for (File configFile : proguardConfigs) {
+ command.add("-include"); //$NON-NLS-1$
+ command.add(quotePath(configFile.getAbsolutePath()));
+ }
+
+ command.add("-injars"); //$NON-NLS-1$
+ StringBuilder sb = new StringBuilder(quotePath(inputJar.getAbsolutePath()));
+ for (String jarFile : jarFiles) {
+ sb.append(File.pathSeparatorChar);
+ sb.append(quotePath(jarFile));
+ }
+ command.add(quoteWinArg(sb.toString()));
+
+ command.add("-outjars"); //$NON-NLS-1$
+ command.add(quotePath(obfuscatedJar.getAbsolutePath()));
+
+ command.add("-libraryjars"); //$NON-NLS-1$
+ sb = new StringBuilder(quotePath(target.getPath(IAndroidTarget.ANDROID_JAR)));
+ IOptionalLibrary[] libraries = target.getOptionalLibraries();
+ if (libraries != null) {
+ for (IOptionalLibrary lib : libraries) {
+ sb.append(File.pathSeparatorChar);
+ sb.append(quotePath(lib.getJarPath()));
+ }
+ }
+ command.add(quoteWinArg(sb.toString()));
+
+ if (logOutput != null) {
+ if (logOutput.isDirectory() == false) {
+ logOutput.mkdirs();
+ }
+
+ command.add("-dump"); //$NON-NLS-1$
+ command.add(new File(logOutput, "dump.txt").getAbsolutePath()); //$NON-NLS-1$
+
+ command.add("-printseeds"); //$NON-NLS-1$
+ command.add(new File(logOutput, "seeds.txt").getAbsolutePath()); //$NON-NLS-1$
+
+ command.add("-printusage"); //$NON-NLS-1$
+ command.add(new File(logOutput, "usage.txt").getAbsolutePath()); //$NON-NLS-1$
+
+ command.add("-printmapping"); //$NON-NLS-1$
+ command.add(new File(logOutput, "mapping.txt").getAbsolutePath()); //$NON-NLS-1$
+ }
+
+ String commandArray[] = null;
+
+ if (SdkConstants.currentPlatform() == SdkConstants.PLATFORM_WINDOWS) {
+ commandArray = createWindowsProguardConfig(command);
+ }
+
+ if (commandArray == null) {
+ // For Mac & Linux, use a regular command string array.
+ commandArray = command.toArray(new String[command.size()]);
+ }
+
+ // Define PROGUARD_HOME to point to $SDK/tools/proguard if it's not yet defined.
+ // The Mac/Linux proguard.sh can infer it correctly but not the proguard.bat one.
+ String[] envp = null;
+ Map<String, String> envMap = new TreeMap<String, String>(System.getenv());
+ if (!envMap.containsKey("PROGUARD_HOME")) { //$NON-NLS-1$
+ envMap.put("PROGUARD_HOME", Sdk.getCurrent().getSdkOsLocation() + //$NON-NLS-1$
+ SdkConstants.FD_TOOLS + File.separator +
+ SdkConstants.FD_PROGUARD);
+ envp = new String[envMap.size()];
+ int i = 0;
+ for (Map.Entry<String, String> entry : envMap.entrySet()) {
+ envp[i++] = String.format("%1$s=%2$s", //$NON-NLS-1$
+ entry.getKey(),
+ entry.getValue());
+ }
+ }
+
+ if (AdtPrefs.getPrefs().getBuildVerbosity() == BuildVerbosity.VERBOSE) {
+ sb = new StringBuilder();
+ for (String c : commandArray) {
+ sb.append(c).append(' ');
+ }
+ AdtPlugin.printToConsole(mProject, sb.toString());
+ }
+
+ // launch
+ int execError = 1;
+ try {
+ // launch the command line process
+ Process process = Runtime.getRuntime().exec(commandArray, envp);
+
+ // list to store each line of stderr
+ ArrayList<String> results = new ArrayList<String>();
+
+ // get the output and return code from the process
+ execError = grabProcessOutput(mProject, process, results);
+
+ if (mVerbose) {
+ for (String resultString : results) {
+ mOutStream.println(resultString);
+ }
+ }
+
+ if (execError != 0) {
+ throw new ProguardResultException(execError,
+ results.toArray(new String[results.size()]));
+ }
+
+ } catch (IOException e) {
+ String msg = String.format(Messages.Proguard_Exec_Error, commandArray[0]);
+ throw new ProguardExecException(msg, e);
+ } catch (InterruptedException e) {
+ String msg = String.format(Messages.Proguard_Exec_Error, commandArray[0]);
+ throw new ProguardExecException(msg, e);
+ }
+ }
+
+ /**
+ * For tools R8 up to R11, the proguard.bat launcher on Windows only accepts
+ * arguments %1..%9. Since we generally have about 15 arguments, we were working
+ * around this by generating a temporary config file for proguard and then using
+ * that.
+ * Starting with tools R12, the proguard.bat launcher has been fixed to take
+ * all arguments using %* so we no longer need this hack.
+ *
+ * @param command
+ * @return
+ * @throws IOException
+ */
+ private String[] createWindowsProguardConfig(List<String> command) throws IOException {
+
+ // Arg 0 is the proguard.bat path and arg 1 is the user config file
+ String launcher = AdtPlugin.readFile(new File(command.get(0)));
+ if (launcher.contains("%*")) { //$NON-NLS-1$
+ // This is the launcher from Tools R12. Don't work around it.
+ return null;
+ }
+
+ // On Windows, proguard.bat can only pass %1...%9 to the java -jar proguard.jar
+ // call, but we have at least 15 arguments here so some get dropped silently
+ // and quoting is a big issue. So instead we'll work around that by writing
+ // all the arguments to a temporary config file.
+
+ String[] commandArray = new String[3];
+
+ commandArray[0] = command.get(0);
+ commandArray[1] = command.get(1);
+
+ // Write all the other arguments to a config file
+ File argsFile = File.createTempFile(TEMP_PREFIX, ".pro"); //$NON-NLS-1$
+ // TODO FIXME this may leave a lot of temp files around on a long session.
+ // Should have a better way to clean up e.g. before each build.
+ argsFile.deleteOnExit();
+
+ FileWriter fw = new FileWriter(argsFile);
+
+ for (int i = 2; i < command.size(); i++) {
+ String s = command.get(i);
+ fw.write(s);
+ fw.write(s.startsWith("-") ? ' ' : '\n'); //$NON-NLS-1$
+ }
+
+ fw.close();
+
+ commandArray[2] = "@" + argsFile.getAbsolutePath(); //$NON-NLS-1$
+ return commandArray;
+ }
+
+ /**
+ * Quotes a single path for proguard to deal with spaces.
+ *
+ * @param path The path to quote.
+ * @return The original path if it doesn't contain a space.
+ * Or the original path surrounded by single quotes if it contains spaces.
+ */
+ private String quotePath(String path) {
+ if (path.indexOf(' ') != -1) {
+ path = '\'' + path + '\'';
+ }
+ return path;
+ }
+
+ /**
+ * Quotes a compound proguard argument to deal with spaces.
+ * <p/>
+ * Proguard takes multi-path arguments such as "path1;path2" for some options.
+ * When the {@link #quotePath} methods adds quotes for such a path if it contains spaces,
+ * the proguard shell wrapper will absorb the quotes, so we need to quote around the
+ * quotes.
+ *
+ * @param path The path to quote.
+ * @return The original path if it doesn't contain a single quote.
+ * Or on Windows the original path surrounded by double quotes if it contains a quote.
+ */
+ private String quoteWinArg(String path) {
+ if (path.indexOf('\'') != -1 &&
+ SdkConstants.currentPlatform() == SdkConstants.PLATFORM_WINDOWS) {
+ path = '"' + path + '"';
+ }
+ return path;
+ }
+
+
+ /**
+ * Execute the Dx tool for dalvik code conversion.
+ * @param javaProject The java project
+ * @param inputPaths the input paths for DX
+ * @param osOutFilePath the path of the dex file to create.
+ *
+ * @throws CoreException
+ * @throws DexException
+ */
+ public void executeDx(IJavaProject javaProject, Collection<String> inputPaths,
+ String osOutFilePath)
+ throws CoreException, DexException {
+
+ // get the dex wrapper
+ Sdk sdk = Sdk.getCurrent();
+ DexWrapper wrapper = sdk.getDexWrapper(mBuildToolInfo);
+
+ if (wrapper == null) {
+ throw new CoreException(new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID,
+ Messages.ApkBuilder_UnableBuild_Dex_Not_loaded));
+ }
+
+ try {
+ // set a temporary prefix on the print streams.
+ mOutStream.setPrefix(CONSOLE_PREFIX_DX);
+ mErrStream.setPrefix(CONSOLE_PREFIX_DX);
+
+ IFolder binFolder = BaseProjectHelper.getAndroidOutputFolder(javaProject.getProject());
+ File binFile = binFolder.getLocation().toFile();
+ File dexedLibs = new File(binFile, "dexedLibs");
+ if (dexedLibs.exists() == false) {
+ dexedLibs.mkdir();
+ }
+
+ // replace the libs by their dexed versions (dexing them if needed.)
+ List<String> finalInputPaths = new ArrayList<String>(inputPaths.size());
+ if (mDisableDexMerger || inputPaths.size() == 1) {
+ // only one input, no need to put a pre-dexed version, even if this path is
+ // just a jar file (case for proguard'ed builds)
+ finalInputPaths.addAll(inputPaths);
+ } else {
+
+ for (String input : inputPaths) {
+ File inputFile = new File(input);
+ if (inputFile.isDirectory()) {
+ finalInputPaths.add(input);
+ } else if (inputFile.isFile()) {
+ String fileName = getDexFileName(inputFile);
+
+ File dexedLib = new File(dexedLibs, fileName);
+ String dexedLibPath = dexedLib.getAbsolutePath();
+
+ if (dexedLib.isFile() == false ||
+ dexedLib.lastModified() < inputFile.lastModified()) {
+
+ if (mVerbose) {
+ mOutStream.println(
+ String.format("Pre-Dexing %1$s -> %2$s", input, fileName));
+ }
+
+ if (dexedLib.isFile()) {
+ dexedLib.delete();
+ }
+
+ int res = wrapper.run(dexedLibPath, Collections.singleton(input),
+ mForceJumbo, mVerbose, mOutStream, mErrStream);
+
+ if (res != 0) {
+ // output error message and mark the project.
+ String message = String.format(Messages.Dalvik_Error_d, res);
+ throw new DexException(message);
+ }
+ } else {
+ if (mVerbose) {
+ mOutStream.println(
+ String.format("Using Pre-Dexed %1$s <- %2$s",
+ fileName, input));
+ }
+ }
+
+ finalInputPaths.add(dexedLibPath);
+ }
+ }
+ }
+
+ if (mVerbose) {
+ for (String input : finalInputPaths) {
+ mOutStream.println("Input: " + input);
+ }
+ }
+
+ int res = wrapper.run(osOutFilePath,
+ finalInputPaths,
+ mForceJumbo,
+ mVerbose,
+ mOutStream, mErrStream);
+
+ mOutStream.setPrefix(null);
+ mErrStream.setPrefix(null);
+
+ if (res != 0) {
+ // output error message and marker the project.
+ String message = String.format(Messages.Dalvik_Error_d, res);
+ throw new DexException(message);
+ }
+ } catch (DexException e) {
+ throw e;
+ } catch (Throwable t) {
+ String message = t.getMessage();
+ if (message == null) {
+ message = t.getClass().getCanonicalName();
+ }
+ message = String.format(Messages.Dalvik_Error_s, message);
+
+ throw new DexException(message, t);
+ }
+ }
+
+ private String getDexFileName(File inputFile) {
+ // get the filename
+ String name = inputFile.getName();
+ // remove the extension
+ int pos = name.lastIndexOf('.');
+ if (pos != -1) {
+ name = name.substring(0, pos);
+ }
+
+ // add a hash of the original file path
+ HashFunction hashFunction = Hashing.md5();
+ HashCode hashCode = hashFunction.hashString(inputFile.getAbsolutePath(), Charsets.UTF_8);
+
+ return name + "-" + hashCode.toString() + ".jar";
+ }
+
+ /**
+ * Executes aapt. If any error happen, files or the project will be marked.
+ * @param command The command for aapt to execute. Currently supported: package and crunch
+ * @param osManifestPath The path to the manifest file
+ * @param osResPath The path to the res folder
+ * @param osAssetsPath The path to the assets folder. This can be null.
+ * @param osOutFilePath The path to the temporary resource file to create,
+ * or in the case of crunching the path to the cache to create/update.
+ * @param configFilter The configuration filter for the resources to include
+ * (used with -c option, for example "port,en,fr" to include portrait, English and French
+ * resources.)
+ * @param versionCode optional version code to insert in the manifest during packaging. If <=0
+ * then no value is inserted
+ * @throws AaptExecException
+ * @throws AaptResultException
+ */
+ private void executeAapt(String aaptCommand, String osManifestPath,
+ List<String> osResPaths, String osAssetsPath, String osOutFilePath,
+ String configFilter, int versionCode) throws AaptExecException, AaptResultException {
+ IAndroidTarget target = Sdk.getCurrent().getTarget(mProject);
+
+ String aapt = mBuildToolInfo.getPath(BuildToolInfo.PathId.AAPT);
+
+ // Create the command line.
+ ArrayList<String> commandArray = new ArrayList<String>();
+ commandArray.add(aapt);
+ commandArray.add(aaptCommand);
+ if (AdtPrefs.getPrefs().getBuildVerbosity() == BuildVerbosity.VERBOSE) {
+ commandArray.add("-v"); //$NON-NLS-1$
+ }
+
+ // Common to all commands
+ for (String path : osResPaths) {
+ commandArray.add("-S"); //$NON-NLS-1$
+ commandArray.add(path);
+ }
+
+ if (aaptCommand.equals(COMMAND_PACKAGE)) {
+ commandArray.add("-f"); //$NON-NLS-1$
+ commandArray.add("--no-crunch"); //$NON-NLS-1$
+
+ // if more than one res, this means there's a library (or more) and we need
+ // to activate the auto-add-overlay
+ if (osResPaths.size() > 1) {
+ commandArray.add("--auto-add-overlay"); //$NON-NLS-1$
+ }
+
+ if (mDebugMode) {
+ commandArray.add("--debug-mode"); //$NON-NLS-1$
+ }
+
+ if (versionCode > 0) {
+ commandArray.add("--version-code"); //$NON-NLS-1$
+ commandArray.add(Integer.toString(versionCode));
+ }
+
+ if (configFilter != null) {
+ commandArray.add("-c"); //$NON-NLS-1$
+ commandArray.add(configFilter);
+ }
+
+ // never compress apks.
+ commandArray.add("-0");
+ commandArray.add("apk");
+
+ commandArray.add("-M"); //$NON-NLS-1$
+ commandArray.add(osManifestPath);
+
+ if (osAssetsPath != null) {
+ commandArray.add("-A"); //$NON-NLS-1$
+ commandArray.add(osAssetsPath);
+ }
+
+ commandArray.add("-I"); //$NON-NLS-1$
+ commandArray.add(target.getPath(IAndroidTarget.ANDROID_JAR));
+
+ commandArray.add("-F"); //$NON-NLS-1$
+ commandArray.add(osOutFilePath);
+ } else if (aaptCommand.equals(COMMAND_CRUNCH)) {
+ commandArray.add("-C"); //$NON-NLS-1$
+ commandArray.add(osOutFilePath);
+ }
+
+ String command[] = commandArray.toArray(
+ new String[commandArray.size()]);
+
+ if (AdtPrefs.getPrefs().getBuildVerbosity() == BuildVerbosity.VERBOSE) {
+ StringBuilder sb = new StringBuilder();
+ for (String c : command) {
+ sb.append(c);
+ sb.append(' ');
+ }
+ AdtPlugin.printToConsole(mProject, sb.toString());
+ }
+
+ // Benchmarking start
+ long startAaptTime = 0;
+ if (BENCHMARK_FLAG) {
+ String msg = "BENCHMARK ADT: Starting " + aaptCommand //$NON-NLS-1$
+ + " call to Aapt"; //$NON-NLS-1$
+ AdtPlugin.printBuildToConsole(BuildVerbosity.ALWAYS, mProject, msg);
+ startAaptTime = System.nanoTime();
+ }
+
+ // launch
+ try {
+ // launch the command line process
+ Process process = Runtime.getRuntime().exec(command);
+
+ // list to store each line of stderr
+ ArrayList<String> stdErr = new ArrayList<String>();
+
+ // get the output and return code from the process
+ int returnCode = grabProcessOutput(mProject, process, stdErr);
+
+ if (mVerbose) {
+ for (String stdErrString : stdErr) {
+ mOutStream.println(stdErrString);
+ }
+ }
+ if (returnCode != 0) {
+ throw new AaptResultException(returnCode,
+ stdErr.toArray(new String[stdErr.size()]));
+ }
+ } catch (IOException e) {
+ String msg = String.format(Messages.AAPT_Exec_Error_s, command[0]);
+ throw new AaptExecException(msg, e);
+ } catch (InterruptedException e) {
+ String msg = String.format(Messages.AAPT_Exec_Error_s, command[0]);
+ throw new AaptExecException(msg, e);
+ }
+
+ // Benchmarking end
+ if (BENCHMARK_FLAG) {
+ String msg = "BENCHMARK ADT: Ending " + aaptCommand //$NON-NLS-1$
+ + " call to Aapt.\nBENCHMARK ADT: Time Elapsed: " //$NON-NLS-1$
+ + ((System.nanoTime() - startAaptTime)/MILLION) + "ms"; //$NON-NLS-1$
+ AdtPlugin.printBuildToConsole(BuildVerbosity.ALWAYS, mProject, msg);
+ }
+ }
+
+ /**
+ * Computes all the project output and dependencies that must go into building the apk.
+ *
+ * @param resMarker
+ * @throws CoreException
+ */
+ private void gatherPaths(ResourceMarker resMarker)
+ throws CoreException {
+ IWorkspaceRoot wsRoot = ResourcesPlugin.getWorkspace().getRoot();
+
+ // get a java project for the project.
+ IJavaProject javaProject = JavaCore.create(mProject);
+
+
+ // get the output of the main project
+ IPath path = javaProject.getOutputLocation();
+ IResource outputResource = wsRoot.findMember(path);
+ if (outputResource != null && outputResource.getType() == IResource.FOLDER) {
+ mCompiledCodePaths.add(outputResource.getLocation().toOSString());
+ }
+
+ // we could use IJavaProject.getResolvedClasspath directly, but we actually
+ // want to see the containers themselves.
+ IClasspathEntry[] classpaths = javaProject.readRawClasspath();
+ if (classpaths != null) {
+ for (IClasspathEntry e : classpaths) {
+ // ignore non exported entries, unless they're in the DEPEDENCIES container,
+ // in which case we always want it (there may be some older projects that
+ // have it as non exported).
+ if (e.isExported() ||
+ (e.getEntryKind() == IClasspathEntry.CPE_CONTAINER &&
+ e.getPath().toString().equals(AdtConstants.CONTAINER_DEPENDENCIES))) {
+ handleCPE(e, javaProject, wsRoot, resMarker);
+ }
+ }
+ }
+ }
+
+ private void handleCPE(IClasspathEntry entry, IJavaProject javaProject,
+ IWorkspaceRoot wsRoot, ResourceMarker resMarker) {
+
+ // if this is a classpath variable reference, we resolve it.
+ if (entry.getEntryKind() == IClasspathEntry.CPE_VARIABLE) {
+ entry = JavaCore.getResolvedClasspathEntry(entry);
+ }
+
+ if (entry.getEntryKind() == IClasspathEntry.CPE_PROJECT) {
+ IProject refProject = wsRoot.getProject(entry.getPath().lastSegment());
+ try {
+ // ignore if it's an Android project, or if it's not a Java Project
+ if (refProject.hasNature(JavaCore.NATURE_ID) &&
+ refProject.hasNature(AdtConstants.NATURE_DEFAULT) == false) {
+ IJavaProject refJavaProject = JavaCore.create(refProject);
+
+ // get the output folder
+ IPath path = refJavaProject.getOutputLocation();
+ IResource outputResource = wsRoot.findMember(path);
+ if (outputResource != null && outputResource.getType() == IResource.FOLDER) {
+ mCompiledCodePaths.add(outputResource.getLocation().toOSString());
+ }
+ }
+ } catch (CoreException exception) {
+ // can't query the project nature? ignore
+ }
+
+ } else if (entry.getEntryKind() == IClasspathEntry.CPE_LIBRARY) {
+ handleClasspathLibrary(entry, wsRoot, resMarker);
+ } else if (entry.getEntryKind() == IClasspathEntry.CPE_CONTAINER) {
+ // get the container
+ try {
+ IClasspathContainer container = JavaCore.getClasspathContainer(
+ entry.getPath(), javaProject);
+ // ignore the system and default_system types as they represent
+ // libraries that are part of the runtime.
+ if (container != null && container.getKind() == IClasspathContainer.K_APPLICATION) {
+ IClasspathEntry[] entries = container.getClasspathEntries();
+ for (IClasspathEntry cpe : entries) {
+ handleCPE(cpe, javaProject, wsRoot, resMarker);
+ }
+ }
+ } catch (JavaModelException jme) {
+ // can't resolve the container? ignore it.
+ AdtPlugin.log(jme, "Failed to resolve ClasspathContainer: %s", entry.getPath());
+ }
+ }
+ }
+
+ private void handleClasspathLibrary(IClasspathEntry e, IWorkspaceRoot wsRoot,
+ ResourceMarker resMarker) {
+ // get the IPath
+ IPath path = e.getPath();
+
+ IResource resource = wsRoot.findMember(path);
+
+ if (resource != null && resource.getType() == IResource.PROJECT) {
+ // if it's a project we should just ignore it because it's going to be added
+ // later when we add all the referenced projects.
+
+ } else if (SdkConstants.EXT_JAR.equalsIgnoreCase(path.getFileExtension())) {
+ // case of a jar file (which could be relative to the workspace or a full path)
+ if (resource != null && resource.exists() &&
+ resource.getType() == IResource.FILE) {
+ mCompiledCodePaths.add(resource.getLocation().toOSString());
+ } else {
+ // if the jar path doesn't match a workspace resource,
+ // then we get an OSString and check if this links to a valid file.
+ String osFullPath = path.toOSString();
+
+ File f = new File(osFullPath);
+ if (f.isFile()) {
+ mCompiledCodePaths.add(osFullPath);
+ } else {
+ String message = String.format( Messages.Couldnt_Locate_s_Error,
+ path);
+ // always output to the console
+ mOutStream.println(message);
+
+ // put a marker
+ if (resMarker != null) {
+ resMarker.setWarning(mProject, message);
+ }
+ }
+ }
+ } else {
+ // this can be the case for a class folder.
+ if (resource != null && resource.exists() &&
+ resource.getType() == IResource.FOLDER) {
+ mCompiledCodePaths.add(resource.getLocation().toOSString());
+ } else {
+ // if the path doesn't match a workspace resource,
+ // then we get an OSString and check if this links to a valid folder.
+ String osFullPath = path.toOSString();
+
+ File f = new File(osFullPath);
+ if (f.isDirectory()) {
+ mCompiledCodePaths.add(osFullPath);
+ }
+ }
+ }
+ }
+
+ /**
+ * Checks a {@link IFile} to make sure it should be packaged as standard resources.
+ * @param file the IFile representing the file.
+ * @return true if the file should be packaged as standard java resources.
+ */
+ public static boolean checkFileForPackaging(IFile file) {
+ String name = file.getName();
+
+ String ext = file.getFileExtension();
+ return ApkBuilder.checkFileForPackaging(name, ext);
+ }
+
+ /**
+ * Checks whether an {@link IFolder} and its content is valid for packaging into the .apk as
+ * standard Java resource.
+ * @param folder the {@link IFolder} to check.
+ */
+ public static boolean checkFolderForPackaging(IFolder folder) {
+ String name = folder.getName();
+ return ApkBuilder.checkFolderForPackaging(name);
+ }
+
+ /**
+ * Returns a list of {@link IJavaProject} matching the provided {@link IProject} objects.
+ * @param projects the IProject objects.
+ * @return a new list object containing the IJavaProject object for the given IProject objects.
+ * @throws CoreException
+ */
+ public static List<IJavaProject> getJavaProjects(List<IProject> projects) throws CoreException {
+ ArrayList<IJavaProject> list = new ArrayList<IJavaProject>();
+
+ for (IProject p : projects) {
+ if (p.isOpen() && p.hasNature(JavaCore.NATURE_ID)) {
+
+ list.add(JavaCore.create(p));
+ }
+ }
+
+ return list;
+ }
+
+ /**
+ * Get the stderr output of a process and return when the process is done.
+ * @param process The process to get the output from
+ * @param stderr The array to store the stderr output
+ * @return the process return code.
+ * @throws InterruptedException
+ */
+ public final static int grabProcessOutput(
+ final IProject project,
+ final Process process,
+ final ArrayList<String> stderr)
+ throws InterruptedException {
+
+ return GrabProcessOutput.grabProcessOutput(
+ process,
+ Wait.WAIT_FOR_READERS, // we really want to make sure we get all the output!
+ new IProcessOutput() {
+
+ @SuppressWarnings("unused")
+ @Override
+ public void out(@Nullable String line) {
+ if (line != null) {
+ // If benchmarking always print the lines that
+ // correspond to benchmarking info returned by ADT
+ if (BENCHMARK_FLAG && line.startsWith("BENCHMARK:")) { //$NON-NLS-1$
+ AdtPlugin.printBuildToConsole(BuildVerbosity.ALWAYS,
+ project, line);
+ } else {
+ AdtPlugin.printBuildToConsole(BuildVerbosity.VERBOSE,
+ project, line);
+ }
+ }
+ }
+
+ @Override
+ public void err(@Nullable String line) {
+ if (line != null) {
+ stderr.add(line);
+ if (BuildVerbosity.VERBOSE == AdtPrefs.getPrefs().getBuildVerbosity()) {
+ AdtPlugin.printErrorToConsole(project, line);
+ }
+ }
+ }
+ });
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/ConvertSwitchDialog.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/ConvertSwitchDialog.java
new file mode 100644
index 000000000..20bee5dac
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/ConvertSwitchDialog.java
@@ -0,0 +1,144 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.ide.eclipse.adt.internal.build;
+
+import com.android.ide.eclipse.adt.internal.editors.IconFactory;
+
+import org.eclipse.jface.dialogs.IDialogConstants;
+import org.eclipse.jface.dialogs.IMessageProvider;
+import org.eclipse.jface.dialogs.MessageDialog;
+import org.eclipse.jface.dialogs.TitleAreaDialog;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.events.SelectionEvent;
+import org.eclipse.swt.events.SelectionListener;
+import org.eclipse.swt.graphics.Image;
+import org.eclipse.swt.graphics.Point;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.layout.GridLayout;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Control;
+import org.eclipse.swt.widgets.Link;
+import org.eclipse.swt.widgets.Shell;
+import org.eclipse.ui.IWorkbench;
+import org.eclipse.ui.PlatformUI;
+import org.eclipse.ui.browser.IWebBrowser;
+
+import java.net.URL;
+
+/**
+ * Dialog shown by the {@link ConvertSwitchQuickFixProcessor}. This is a custom
+ * dialog rather than a plain {@link MessageDialog} such that we can show a link
+ * and point to a web page for more info.
+ */
+class ConvertSwitchDialog extends TitleAreaDialog implements SelectionListener {
+ /** URL containing more info */
+ private static final String URL = "http://tools.android.com/tips/non-constant-fields"; //$NON-NLS-1$
+
+ private final String mField;
+
+ private Link mLink;
+
+ /**
+ * Create the dialog.
+ * @param parentShell the parent shell
+ * @param field the field name we're warning about
+ */
+ public ConvertSwitchDialog(Shell parentShell, String field) {
+ super(parentShell);
+ mField = field;
+ Image image = IconFactory.getInstance().getIcon("android-64"); //$NON-NLS-1$
+ setTitleImage(image);
+ }
+
+ @Override
+ protected Control createDialogArea(Composite parent) {
+ String text = String.format(
+ "As of ADT 14, the resource fields (such as %1$s) are no longer constants " +
+ "when defined in library projects. This is necessary to make library " +
+ "projects reusable without recompiling them.\n" +
+ "\n" +
+ "One consequence of this is that you can no longer use the fields directly " +
+ "in switch statements. You must use an if-else chain instead.\n" +
+ "\n" +
+ "Eclipse can automatically convert from a switch statement to an if-else " +
+ "statement. Just place the caret on the switch keyword and invoke " +
+ "Quick Fix (Ctrl-1 on Windows and Linux, Cmd-1 on Mac), then select " +
+ "\"Convert 'switch' to 'if-else'\".\n" +
+ "\n" +
+ "For more information, see <a href=\"" + URL + "\">" + URL + "</a>",
+ mField);
+
+ Composite area = (Composite) super.createDialogArea(parent);
+ Composite container = new Composite(area, SWT.NONE);
+ container.setLayout(new GridLayout(1, false));
+ container.setLayoutData(new GridData(GridData.FILL_BOTH));
+
+ mLink = new Link(container, SWT.NONE);
+ mLink.setLayoutData(new GridData(SWT.LEFT, SWT.TOP, true, true, 1, 1));
+ mLink.setText(text);
+ mLink.addSelectionListener(this);
+
+ setMessage("Non-Constant Expressions: Migration Necessary", IMessageProvider.INFORMATION);
+
+ return area;
+ }
+
+ @Override
+ protected void createButtonsForButtonBar(Composite parent) {
+ createButton(parent, IDialogConstants.OK_ID, IDialogConstants.OK_LABEL, true);
+ createButton(parent, IDialogConstants.HELP_ID, IDialogConstants.HELP_LABEL, false);
+ }
+
+ @Override
+ protected Point getInitialSize() {
+ return new Point(500, 400);
+ }
+
+ private void showWebPage() {
+ try {
+ IWorkbench workbench = PlatformUI.getWorkbench();
+ IWebBrowser browser = workbench.getBrowserSupport().getExternalBrowser();
+ browser.openURL(new URL(URL));
+ } catch (Exception e) {
+ String message = String.format("Could not open browser. Vist\n%1$s\ninstead.",
+ URL);
+ MessageDialog.openError(getShell(), "Browser Error", message);
+ }
+
+ }
+
+ @Override
+ protected void buttonPressed(int buttonId) {
+ if (buttonId == IDialogConstants.HELP_ID) {
+ showWebPage();
+ } else {
+ super.buttonPressed(buttonId);
+ }
+ }
+
+ // ---- Implements SelectionListener ----
+
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ if (e.getSource() == mLink) {
+ showWebPage();
+ }
+ }
+
+ @Override
+ public void widgetDefaultSelected(SelectionEvent e) {
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/ConvertSwitchQuickFixProcessor.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/ConvertSwitchQuickFixProcessor.java
new file mode 100644
index 000000000..a99dc7601
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/ConvertSwitchQuickFixProcessor.java
@@ -0,0 +1,224 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.internal.build;
+
+import com.android.ide.eclipse.adt.AdtPlugin;
+import com.android.ide.eclipse.adt.AdtUtils;
+
+import org.eclipse.core.resources.IMarker;
+import org.eclipse.core.resources.IResource;
+import org.eclipse.core.runtime.CoreException;
+import org.eclipse.jdt.core.IBuffer;
+import org.eclipse.jdt.core.ICompilationUnit;
+import org.eclipse.jdt.core.compiler.IProblem;
+import org.eclipse.jdt.core.dom.ASTNode;
+import org.eclipse.jdt.core.dom.QualifiedName;
+import org.eclipse.jdt.ui.text.java.IInvocationContext;
+import org.eclipse.jdt.ui.text.java.IJavaCompletionProposal;
+import org.eclipse.jdt.ui.text.java.IProblemLocation;
+import org.eclipse.jdt.ui.text.java.IQuickFixProcessor;
+import org.eclipse.jface.text.IDocument;
+import org.eclipse.jface.text.contentassist.IContextInformation;
+import org.eclipse.swt.graphics.Image;
+import org.eclipse.swt.graphics.Point;
+import org.eclipse.swt.widgets.Shell;
+import org.eclipse.ui.editors.text.TextFileDocumentProvider;
+import org.eclipse.ui.texteditor.IDocumentProvider;
+
+import java.util.List;
+
+/**
+ * A quickfix processor which looks for "case expressions must be constant
+ * expressions" errors, and if they apply to fields in a class named R, it
+ * assumes this is code related to library projects that are no longer final and
+ * will need to be rewritten to use if-else chains instead.
+ */
+public class ConvertSwitchQuickFixProcessor implements IQuickFixProcessor {
+ /** Constructs a new {@link ConvertSwitchQuickFixProcessor} */
+ public ConvertSwitchQuickFixProcessor() {
+ }
+
+ @Override
+ public boolean hasCorrections(ICompilationUnit cu, int problemId) {
+ return problemId == IProblem.NonConstantExpression;
+ }
+
+ @Override
+ public IJavaCompletionProposal[] getCorrections(IInvocationContext context,
+ IProblemLocation[] location) throws CoreException {
+ if (location == null || location.length == 0) {
+ return null;
+ }
+ ASTNode coveringNode = context.getCoveringNode();
+ if (coveringNode == null) {
+ return null;
+ }
+
+ // Look up the fully qualified name of the non-constant expression, if any, and
+ // make sure it's R-something.
+ if (coveringNode.getNodeType() == ASTNode.SIMPLE_NAME) {
+ coveringNode = coveringNode.getParent();
+ if (coveringNode == null) {
+ return null;
+ }
+ }
+ if (coveringNode.getNodeType() != ASTNode.QUALIFIED_NAME) {
+ return null;
+ }
+ QualifiedName name = (QualifiedName) coveringNode;
+ if (!name.getFullyQualifiedName().startsWith("R.")) { //$NON-NLS-1$
+ return null;
+ }
+
+ IProblemLocation error = location[0];
+ int errorStart = error.getOffset();
+ int errorLength = error.getLength();
+ int caret = context.getSelectionOffset();
+
+ // Even though the hasCorrections() method above will return false for everything
+ // other than non-constant expression errors, it turns out this getCorrections()
+ // method will ALSO be called on lines where there is no such error. In particular,
+ // if you have an invalid cast expression like this:
+ // Button button = findViewById(R.id.textView);
+ // then this method will be called, and the expression will pass all of the above
+ // checks. However, we -don't- want to show a migrate code suggestion in that case!
+ // Therefore, we'll need to check if we're *actually* on a line with the given
+ // problem.
+ //
+ // Unfortunately, we don't get passed the problemId again, and there's no access
+ // to it. So instead we'll need to look up the markers on the line, and see
+ // if we actually have a constant expression warning. This is not pretty!!
+
+ boolean foundError = false;
+ ICompilationUnit compilationUnit = context.getCompilationUnit();
+ IResource file = compilationUnit.getResource();
+ if (file != null) {
+ IDocumentProvider provider = new TextFileDocumentProvider();
+ try {
+ provider.connect(file);
+ IDocument document = provider.getDocument(file);
+ if (document != null) {
+ List<IMarker> markers = AdtUtils.findMarkersOnLine(IMarker.PROBLEM,
+ file, document, errorStart);
+ for (IMarker marker : markers) {
+ String message = marker.getAttribute(IMarker.MESSAGE, "");
+ // There are no other attributes in the marker we can use to identify
+ // the exact error, so we'll need to resort to the actual message
+ // text even though that would not work if the messages had been
+ // localized... This can also break if the error messages change. Yuck.
+ if (message.contains("constant expressions")) { //$NON-NLS-1$
+ foundError = true;
+ }
+ }
+ }
+ } catch (Exception e) {
+ AdtPlugin.log(e, "Can't validate error message in %1$s", file.getName());
+ } finally {
+ provider.disconnect(file);
+ }
+ }
+ if (!foundError) {
+ // Not a constant-expression warning, so do nothing
+ return null;
+ }
+
+ IBuffer buffer = compilationUnit.getBuffer();
+ boolean sameLine = false;
+ // See if the caret is on the same line as the error
+ if (caret <= errorStart) {
+ // Search backwards to beginning of line
+ for (int i = errorStart; i >= 0; i--) {
+ if (i <= caret) {
+ sameLine = true;
+ break;
+ }
+ char c = buffer.getChar(i);
+ if (c == '\n') {
+ break;
+ }
+ }
+ } else {
+ // Search forwards to the end of the line
+ for (int i = errorStart + errorLength, n = buffer.getLength(); i < n; i++) {
+ if (i >= caret) {
+ sameLine = true;
+ break;
+ }
+ char c = buffer.getChar(i);
+ if (c == '\n') {
+ break;
+ }
+ }
+ }
+
+ if (sameLine) {
+ String expression = buffer.getText(errorStart, errorLength);
+ return new IJavaCompletionProposal[] {
+ new MigrateProposal(expression)
+ };
+ }
+
+ return null;
+ }
+
+ /** Proposal for the quick fix which displays an explanation message to the user */
+ private class MigrateProposal implements IJavaCompletionProposal {
+ private String mExpression;
+
+ private MigrateProposal(String expression) {
+ mExpression = expression;
+ }
+
+ @Override
+ public void apply(IDocument document) {
+ Shell shell = AdtPlugin.getShell();
+ ConvertSwitchDialog dialog = new ConvertSwitchDialog(shell, mExpression);
+ dialog.open();
+ }
+
+ @Override
+ public Point getSelection(IDocument document) {
+ return null;
+ }
+
+ @Override
+ public String getAdditionalProposalInfo() {
+ return "As of ADT 14, resource fields cannot be used as switch cases. Invoke this " +
+ "fix to get more information.";
+ }
+
+ @Override
+ public String getDisplayString() {
+ return "Migrate Android Code";
+ }
+
+ @Override
+ public Image getImage() {
+ return AdtPlugin.getAndroidLogo();
+ }
+
+ @Override
+ public IContextInformation getContextInformation() {
+ return null;
+ }
+
+ @Override
+ public int getRelevance() {
+ return 50;
+ }
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/DefaultSourceChangeHandler.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/DefaultSourceChangeHandler.java
new file mode 100644
index 000000000..ea0d695d1
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/DefaultSourceChangeHandler.java
@@ -0,0 +1,105 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.internal.build;
+
+import org.eclipse.core.resources.IContainer;
+import org.eclipse.core.resources.IFile;
+import org.eclipse.core.resources.IResourceDelta;
+
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * Base source change handler for the {@link SourceProcessor} classes.
+ *
+ * It can be used as is, as long as the matching {@link SourceProcessor} properly implements
+ * its abstract methods, and the processor does not output resource files,
+ * or can be extended to provide custom implementation for:
+ * {@link #handleSourceFile(IFile, int)}
+ * {@link #handleGeneratedFile(IFile, int)}
+ * {@link #handleResourceFile(IFile, int)}
+ * {@link #filterResourceFolder(IContainer)}
+ *
+ */
+public class DefaultSourceChangeHandler implements SourceChangeHandler {
+
+ private SourceProcessor mProcessor;
+
+ /** List of source files found that are modified or new. */
+ private final Set<IFile> mToCompile = new HashSet<IFile>();
+
+ /** List of source files that have been removed. */
+ private final Set<IFile> mRemoved = new HashSet<IFile>();
+
+ @Override
+ public boolean handleGeneratedFile(IFile file, int kind) {
+ if (kind == IResourceDelta.REMOVED || kind == IResourceDelta.CHANGED) {
+ IFile sourceFile = mProcessor.isOutput(file);
+ if (sourceFile != null) {
+ mToCompile.add(sourceFile);
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ @Override
+ public void handleSourceFile(IFile file, int kind) {
+ // first the file itself if this is a match for the processor's extension
+ if (mProcessor.getExtensions().contains(file.getFileExtension())) {
+ if (kind == IResourceDelta.REMOVED) {
+ mRemoved.add(file);
+ } else {
+ mToCompile.add(file);
+ }
+ }
+
+ // now the dependencies. In all case we compile the files that depend on the
+ // added/changed/removed file.
+ mToCompile.addAll(mProcessor.isDependency(file));
+ }
+
+ protected void addFileToCompile(IFile file) {
+ mToCompile.add(file);
+ }
+
+ Set<IFile> getFilesToCompile() {
+ return mToCompile;
+ }
+
+ protected void addRemovedFile(IFile file) {
+ mRemoved.add(file);
+ }
+
+ Set<IFile> getRemovedFiles() {
+ return mRemoved;
+ }
+
+ public void reset() {
+ mToCompile.clear();
+ mRemoved.clear();
+ }
+
+ protected SourceProcessor getProcessor() {
+ return mProcessor;
+ }
+
+ void init(SourceProcessor processor) {
+ mProcessor = processor;
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/DexException.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/DexException.java
new file mode 100644
index 000000000..032e5e646
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/DexException.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.internal.build;
+
+/**
+ * Exception throw when dx fails.
+ *
+ */
+public final class DexException extends Exception {
+ private static final long serialVersionUID = 1L;
+
+ DexException(String message) {
+ super(message);
+ }
+
+ DexException(String message, Throwable cause) {
+ super(message, cause);
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/DexWrapper.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/DexWrapper.java
new file mode 100644
index 000000000..3f882842b
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/DexWrapper.java
@@ -0,0 +1,218 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.internal.build;
+
+import com.android.SdkConstants;
+import com.android.ide.eclipse.adt.AdtPlugin;
+
+import org.eclipse.core.runtime.CoreException;
+import org.eclipse.core.runtime.IStatus;
+import org.eclipse.core.runtime.Status;
+
+import java.io.File;
+import java.io.PrintStream;
+import java.lang.reflect.Constructor;
+import java.lang.reflect.Field;
+import java.lang.reflect.Method;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.net.URLClassLoader;
+import java.util.Collection;
+
+/**
+ * Wrapper to access dx.jar through reflection.
+ * <p/>Since there is no proper api to call the method in the dex library, this wrapper is going
+ * to access it through reflection.
+ */
+public final class DexWrapper {
+
+ private final static String DEX_MAIN = "com.android.dx.command.dexer.Main"; //$NON-NLS-1$
+ private final static String DEX_CONSOLE = "com.android.dx.command.DxConsole"; //$NON-NLS-1$
+ private final static String DEX_ARGS = "com.android.dx.command.dexer.Main$Arguments"; //$NON-NLS-1$
+
+ private final static String MAIN_RUN = "run"; //$NON-NLS-1$
+
+ private Method mRunMethod;
+
+ private Constructor<?> mArgConstructor;
+ private Field mArgOutName;
+ private Field mArgVerbose;
+ private Field mArgJarOutput;
+ private Field mArgFileNames;
+ private Field mArgForceJumbo;
+
+ private Field mConsoleOut;
+ private Field mConsoleErr;
+
+ /**
+ * Loads the dex library from a file path.
+ *
+ * The loaded library can be used via
+ * {@link DexWrapper#run(String, String[], boolean, PrintStream, PrintStream)}.
+ *
+ * @param osFilepath the location of the dx.jar file.
+ * @return an IStatus indicating the result of the load.
+ */
+ public synchronized IStatus loadDex(String osFilepath) {
+ try {
+ File f = new File(osFilepath);
+ if (f.isFile() == false) {
+ return new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID, String.format(
+ Messages.DexWrapper_s_does_not_exists, osFilepath));
+ }
+ URL url = f.toURI().toURL();
+
+ @SuppressWarnings("resource")
+ URLClassLoader loader = new URLClassLoader(new URL[] { url },
+ DexWrapper.class.getClassLoader());
+
+ // get the classes.
+ Class<?> mainClass = loader.loadClass(DEX_MAIN);
+ Class<?> consoleClass = loader.loadClass(DEX_CONSOLE);
+ Class<?> argClass = loader.loadClass(DEX_ARGS);
+
+ try {
+ // now get the fields/methods we need
+ mRunMethod = mainClass.getMethod(MAIN_RUN, argClass);
+
+ mArgConstructor = argClass.getConstructor();
+ mArgOutName = argClass.getField("outName"); //$NON-NLS-1$
+ mArgJarOutput = argClass.getField("jarOutput"); //$NON-NLS-1$
+ mArgFileNames = argClass.getField("fileNames"); //$NON-NLS-1$
+ mArgVerbose = argClass.getField("verbose"); //$NON-NLS-1$
+ mArgForceJumbo = argClass.getField("forceJumbo"); //$NON-NLS-1$
+
+ mConsoleOut = consoleClass.getField("out"); //$NON-NLS-1$
+ mConsoleErr = consoleClass.getField("err"); //$NON-NLS-1$
+
+ } catch (SecurityException e) {
+ return createErrorStatus(Messages.DexWrapper_SecuryEx_Unable_To_Find_API, e);
+ } catch (NoSuchMethodException e) {
+ return createErrorStatus(Messages.DexWrapper_SecuryEx_Unable_To_Find_Method, e);
+ } catch (NoSuchFieldException e) {
+ return createErrorStatus(Messages.DexWrapper_SecuryEx_Unable_To_Find_Field, e);
+ }
+
+ return Status.OK_STATUS;
+ } catch (MalformedURLException e) {
+ // really this should not happen.
+ return createErrorStatus(
+ String.format(Messages.DexWrapper_Failed_to_load_s, osFilepath), e);
+ } catch (ClassNotFoundException e) {
+ return createErrorStatus(
+ String.format(Messages.DexWrapper_Failed_to_load_s, osFilepath), e);
+ }
+ }
+
+ /**
+ * Removes any reference to the dex library.
+ * <p/>
+ * {@link #loadDex(String)} must be called on the wrapper
+ * before {@link #run(String, String[], boolean, PrintStream, PrintStream)} can
+ * be used again.
+ */
+ public synchronized void unload() {
+ mRunMethod = null;
+ mArgConstructor = null;
+ mArgOutName = null;
+ mArgJarOutput = null;
+ mArgFileNames = null;
+ mArgVerbose = null;
+ mConsoleOut = null;
+ mConsoleErr = null;
+ System.gc();
+ }
+
+ /**
+ * Runs the dex command.
+ * The wrapper must have been initialized via {@link #loadDex(String)} first.
+ *
+ * @param osOutFilePath the OS path to the outputfile (classes.dex
+ * @param osFilenames list of input source files (.class and .jar files)
+ * @param forceJumbo force jumbo mode.
+ * @param verbose verbose mode.
+ * @param outStream the stdout console
+ * @param errStream the stderr console
+ * @return the integer return code of com.android.dx.command.dexer.Main.run()
+ * @throws CoreException
+ */
+ public synchronized int run(String osOutFilePath, Collection<String> osFilenames,
+ boolean forceJumbo, boolean verbose,
+ PrintStream outStream, PrintStream errStream) throws CoreException {
+
+ assert mRunMethod != null;
+ assert mArgConstructor != null;
+ assert mArgOutName != null;
+ assert mArgJarOutput != null;
+ assert mArgFileNames != null;
+ assert mArgForceJumbo != null;
+ assert mArgVerbose != null;
+ assert mConsoleOut != null;
+ assert mConsoleErr != null;
+
+ if (mRunMethod == null) {
+ throw new CoreException(createErrorStatus(
+ String.format(Messages.DexWrapper_Unable_To_Execute_Dex_s,
+ "wrapper was not properly loaded first"),
+ null /*exception*/));
+ }
+
+ try {
+ // set the stream
+ mConsoleErr.set(null /* obj: static field */, errStream);
+ mConsoleOut.set(null /* obj: static field */, outStream);
+
+ // create the Arguments object.
+ Object args = mArgConstructor.newInstance();
+ mArgOutName.set(args, osOutFilePath);
+ mArgFileNames.set(args, osFilenames.toArray(new String[osFilenames.size()]));
+ mArgJarOutput.set(args, osOutFilePath.endsWith(SdkConstants.DOT_JAR));
+ mArgForceJumbo.set(args, forceJumbo);
+ mArgVerbose.set(args, verbose);
+
+ // call the run method
+ Object res = mRunMethod.invoke(null /* obj: static method */, args);
+
+ if (res instanceof Integer) {
+ return ((Integer)res).intValue();
+ }
+
+ return -1;
+ } catch (Exception e) {
+ Throwable t = e;
+ while (t.getCause() != null) {
+ t = t.getCause();
+ }
+
+ String msg = t.getMessage();
+ if (msg == null) {
+ msg = String.format("%s. Check the Eclipse log for stack trace.",
+ t.getClass().getName());
+ }
+
+ throw new CoreException(createErrorStatus(
+ String.format(Messages.DexWrapper_Unable_To_Execute_Dex_s, msg), t));
+ }
+ }
+
+ private static IStatus createErrorStatus(String message, Throwable e) {
+ AdtPlugin.log(e, message);
+ AdtPlugin.printErrorToConsole(Messages.DexWrapper_Dex_Loader, message);
+
+ return new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID, message, e);
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/ExecResultException.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/ExecResultException.java
new file mode 100644
index 000000000..63a7a6946
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/ExecResultException.java
@@ -0,0 +1,72 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.internal.build;
+
+/**
+ * Base exception class containing the error code and output of an external tool failed exec.
+ *
+ */
+class ExecResultException extends Exception {
+ private static final long serialVersionUID = 1L;
+
+ private final int mErrorCode;
+ private final String[] mOutput;
+
+ protected ExecResultException(int errorCode, String[] output) {
+ mErrorCode = errorCode;
+ mOutput = output;
+ }
+
+ /**
+ * Returns the full output of aapt.
+ */
+ public String[] getOutput() {
+ return mOutput;
+ }
+
+ /**
+ * Returns the aapt return code.
+ */
+ public int getErrorCode() {
+ return mErrorCode;
+ }
+
+ public String getLabel() {
+ return "Command-line";
+ }
+
+ @Override
+ public String toString() {
+ String result = String.format("%1$s Error %2$d", getLabel(), mErrorCode);
+ if (mOutput != null && mOutput.length > 0) {
+ // Note : the "error detail" window in Eclipse seem to ignore the \n,
+ // so we prefix them with a space. It's not optimal but it's slightly readable.
+ result += " \nOutput:";
+ for (String o : mOutput) {
+ if (o != null) {
+ result += " \n" + o;
+ }
+ }
+ }
+ return result;
+ }
+
+ @Override
+ public String getMessage() {
+ return toString();
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/Messages.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/Messages.java
new file mode 100644
index 000000000..9ceba205d
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/Messages.java
@@ -0,0 +1,150 @@
+
+package com.android.ide.eclipse.adt.internal.build;
+
+import org.eclipse.osgi.util.NLS;
+
+public class Messages extends NLS {
+ private static final String BUNDLE_NAME = "com.android.ide.eclipse.adt.internal.build.build_messages"; //$NON-NLS-1$
+
+ public static String AAPT_Error;
+
+ public static String AAPT_Exec_Error_s;
+
+ public static String AAPT_Exec_Error_d;
+
+ public static String AAPT_Exec_Error_Other_s;
+
+ public static String Added_s_s_Needs_Updating;
+
+ public static String AIDL_Exec_Error_s;
+
+ public static String AIDL_Exec_Error_d;
+
+ public static String AIDL_Java_Conflict;
+
+ public static String ApkBuilder_Certificate_Expired_on_s;
+
+ public static String ApkBuilder_JAVA_HOME_is_s;
+
+ public static String ApkBuilder_Packaging_s;
+
+ public static String ApkBuilder_Packaging_s_into_s;
+
+ public static String ApkBuilder_s_Conflict_with_file_s;
+
+ public static String ApkBuilder_Signing_Key_Creation_s;
+
+ public static String ApkBuilder_Unable_To_Gey_Key;
+
+ public static String ApkBuilder_UnableBuild_Dex_Not_loaded;
+
+ public static String ApkBuilder_Update_or_Execute_manually_s;
+
+ public static String ApkBuilder_Using_Default_Key;
+
+ public static String ApkBuilder_Using_s_To_Sign;
+
+ public static String Checking_Package_Change;
+
+ public static String Compiler_Compliance_Error;
+
+ public static String Couldnt_Locate_s_Error;
+
+ public static String Dalvik_Error_d;
+
+ public static String Dalvik_Error_s;
+
+ public static String Delete_Obsolete_Error;
+
+ public static String DexWrapper_Dex_Loader;
+
+ public static String DexWrapper_Failed_to_load_s;
+
+ public static String DexWrapper_s_does_not_exists;
+
+ public static String DexWrapper_SecuryEx_Unable_To_Find_API;
+
+ public static String DexWrapper_SecuryEx_Unable_To_Find_Field;
+
+ public static String DexWrapper_SecuryEx_Unable_To_Find_Method;
+
+ public static String DexWrapper_Unable_To_Execute_Dex_s;
+
+ public static String DX_Jar_Error;
+
+ public static String Failed_To_Get_Output;
+
+ public static String Final_Archive_Error_s;
+
+ public static String Incompatible_VM_Warning;
+
+ public static String Marker_Delete_Error;
+
+ public static String No_SDK_Setup_Error;
+
+ public static String Nothing_To_Compile;
+
+ public static String Output_Missing;
+
+ public static String Package_s_Doesnt_Exist_Error;
+
+ public static String Preparing_Generated_Files;
+
+ public static String Project_Has_Errors;
+
+ public static String Refreshing_Res;
+
+ public static String Removing_Generated_Classes;
+
+ public static String Requires_1_5_Error;
+
+ public static String Requires_Class_Compatibility_s;
+
+ public static String Requires_Compiler_Compliance_s;
+
+ public static String Requires_Source_Compatibility_s;
+
+ public static String s_Contains_Xml_Error;
+
+ public static String s_Doesnt_Declare_Package_Error;
+
+ public static String s_File_Missing;
+
+ public static String s_Missing_Repackaging;
+
+ public static String s_Modified_Manually_Recreating_s;
+
+ public static String s_Modified_Recreating_s;
+
+ public static String s_Removed_Recreating_s;
+
+ public static String s_Removed_s_Needs_Updating;
+
+ public static String Start_Full_Apk_Build;
+
+ public static String Start_Full_Pre_Compiler;
+
+ public static String Skip_Post_Compiler;
+
+ public static String Start_Full_Post_Compiler;
+
+ public static String Start_Inc_Apk_Build;
+
+ public static String Start_Inc_Pre_Compiler;
+
+ public static String Unparsed_AAPT_Errors;
+
+ public static String Unparsed_AIDL_Errors;
+
+ public static String Xml_Error;
+
+ public static String Proguard_Exec_Error;
+
+ static {
+ // initialize resource bundle
+ NLS.initializeMessages(BUNDLE_NAME, Messages.class);
+ }
+
+ private Messages() {
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/NativeLibInJarException.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/NativeLibInJarException.java
new file mode 100644
index 000000000..18f4ae7aa
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/NativeLibInJarException.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.internal.build;
+
+import com.android.sdklib.build.ApkBuilder.JarStatus;
+
+/**
+ * Exception throw when native libraries are detected in jar file.
+ *
+ */
+public final class NativeLibInJarException extends Exception {
+ private static final long serialVersionUID = 1L;
+
+ private final JarStatus mStatus;
+ private final String mLibName;
+ private final String[] mConsoleMsgs;
+
+ NativeLibInJarException(JarStatus status, String message, String libName,
+ String[] consoleMsgs) {
+ super(message);
+ mStatus = status;
+ mLibName = libName;
+ mConsoleMsgs = consoleMsgs;
+ }
+
+ /**
+ * Returns the {@link JarStatus} object containing the information about the libraries that
+ * were found.
+ */
+ public JarStatus getStatus() {
+ return mStatus;
+ }
+
+ /**
+ * Returns the name of the jar file containing the native libraries.
+ */
+ public String getJarName() {
+ return mLibName;
+ }
+
+ /**
+ * Returns additional information that should be shown to the user.
+ */
+ public String[] getAdditionalInfo() {
+ return mConsoleMsgs;
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/ProguardExecException.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/ProguardExecException.java
new file mode 100644
index 000000000..3eb688b3e
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/ProguardExecException.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.internal.build;
+
+/**
+ * Exception thrown when the execution of proguard fails.
+ *
+ */
+public final class ProguardExecException extends Exception {
+ private static final long serialVersionUID = 1L;
+
+ ProguardExecException(String message, Throwable cause) {
+ super(message, cause);
+ }
+}
+
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/ProguardResultException.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/ProguardResultException.java
new file mode 100644
index 000000000..54246b337
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/ProguardResultException.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.internal.build;
+
+/**
+ * Exception thrown when aapt reports an error in the resources.
+ *
+ */
+public final class ProguardResultException extends ExecResultException {
+ private static final long serialVersionUID = 1L;
+
+ ProguardResultException(int errorCode, String[] output) {
+ super(errorCode, output);
+ }
+
+ @Override
+ public String getLabel() {
+ return "Proguard";
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/RenderScriptLauncher.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/RenderScriptLauncher.java
new file mode 100644
index 000000000..1d3c7bd3a
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/RenderScriptLauncher.java
@@ -0,0 +1,244 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.internal.build;
+
+import com.android.annotations.NonNull;
+import com.android.ide.eclipse.adt.AdtConstants;
+import com.android.ide.eclipse.adt.AdtPlugin;
+import com.android.ide.eclipse.adt.internal.project.BaseProjectHelper;
+import com.android.sdklib.build.RenderScriptProcessor.CommandLineLauncher;
+
+import org.eclipse.core.resources.IFile;
+import org.eclipse.core.resources.IFolder;
+import org.eclipse.core.resources.IMarker;
+import org.eclipse.core.resources.IProject;
+import org.eclipse.core.resources.IResource;
+import org.eclipse.core.runtime.CoreException;
+import org.eclipse.core.runtime.IProgressMonitor;
+import org.eclipse.core.runtime.IStatus;
+import org.eclipse.core.runtime.Path;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * A {@link SourceProcessor} for RenderScript files.
+ */
+public class RenderScriptLauncher implements CommandLineLauncher {
+
+ /**
+ * Single line llvm-rs-cc error: {@code <path>:<line>:<col>: <error>}
+ */
+ private static Pattern sLlvmPattern1 = Pattern.compile("^(.+?):(\\d+):(\\d+):\\s(.+)$"); //$NON-NLS-1$
+
+ @NonNull
+ private final IProject mProject;
+ @NonNull
+ private final IFolder mSourceOutFolder;
+ @NonNull
+ private final IFolder mResOutFolder;
+ @NonNull
+ private final IProgressMonitor mMonitor;
+ private final boolean mVerbose;
+
+ public RenderScriptLauncher(
+ @NonNull IProject project,
+ @NonNull IFolder sourceOutFolder,
+ @NonNull IFolder resOutFolder,
+ @NonNull IProgressMonitor monitor,
+ boolean verbose) {
+ mProject = project;
+ mSourceOutFolder = sourceOutFolder;
+ mResOutFolder = resOutFolder;
+ mMonitor = monitor;
+ mVerbose = verbose;
+ }
+
+ @Override
+ public void launch(File executable, List<String> arguments, Map<String, String> envVariableMap)
+ throws IOException, InterruptedException {
+ // do the exec
+ try {
+ if (mVerbose) {
+ StringBuilder sb = new StringBuilder(executable.getAbsolutePath());
+ for (String c : arguments) {
+ sb.append(' ').append(c);
+ }
+ String cmd_line = sb.toString();
+ AdtPlugin.printToConsole(mProject, cmd_line);
+ }
+
+ String[] commandArray = new String[1 + arguments.size()];
+ commandArray[0] = executable.getAbsolutePath();
+ System.arraycopy(arguments.toArray(), 0, commandArray, 1, arguments.size());
+
+ ProcessBuilder processBuilder = new ProcessBuilder(commandArray);
+ Map<String, String> processEnvs = processBuilder.environment();
+ for (Map.Entry<String, String> entry : envVariableMap.entrySet()) {
+ processEnvs.put(entry.getKey(), entry.getValue());
+ }
+
+ Process p = processBuilder.start();
+
+ // list to store each line of stderr
+ ArrayList<String> stdErr = new ArrayList<String>();
+
+ // get the output and return code from the process
+ int returnCode = BuildHelper.grabProcessOutput(mProject, p, stdErr);
+
+ if (stdErr.size() > 0) {
+ // attempt to parse the error output
+ boolean parsingError = parseLlvmOutput(stdErr);
+
+ // If the process failed and we couldn't parse the output
+ // we print a message, mark the project and exit
+ if (returnCode != 0) {
+
+ if (parsingError || mVerbose) {
+ // display the message in the console.
+ if (parsingError) {
+ AdtPlugin.printErrorToConsole(mProject, stdErr.toArray());
+
+ // mark the project
+ BaseProjectHelper.markResource(mProject,
+ AdtConstants.MARKER_RENDERSCRIPT,
+ "Unparsed Renderscript error! Check the console for output.",
+ IMarker.SEVERITY_ERROR);
+ } else {
+ AdtPlugin.printToConsole(mProject, stdErr.toArray());
+ }
+ }
+ return;
+ }
+ } else if (returnCode != 0) {
+ // no stderr output but exec failed.
+ String msg = String.format("Error executing Renderscript: Return code %1$d",
+ returnCode);
+
+ BaseProjectHelper.markResource(mProject, AdtConstants.MARKER_AIDL,
+ msg, IMarker.SEVERITY_ERROR);
+
+ return;
+ }
+ } catch (IOException e) {
+ // mark the project and exit
+ String msg = String.format(
+ "Error executing Renderscript. Please check %1$s is present at %2$s",
+ executable.getName(), executable.getAbsolutePath());
+ AdtPlugin.log(IStatus.ERROR, msg);
+ BaseProjectHelper.markResource(mProject, AdtConstants.MARKER_RENDERSCRIPT, msg,
+ IMarker.SEVERITY_ERROR);
+ throw e;
+ } catch (InterruptedException e) {
+ // mark the project and exit
+ String msg = String.format(
+ "Error executing Renderscript. Please check %1$s is present at %2$s",
+ executable.getName(), executable.getAbsolutePath());
+ AdtPlugin.log(IStatus.ERROR, msg);
+ BaseProjectHelper.markResource(mProject, AdtConstants.MARKER_RENDERSCRIPT, msg,
+ IMarker.SEVERITY_ERROR);
+ throw e;
+ }
+
+ try {
+ mSourceOutFolder.refreshLocal(IResource.DEPTH_ONE, mMonitor);
+ mResOutFolder.refreshLocal(IResource.DEPTH_ONE, mMonitor);
+ } catch (CoreException e) {
+ AdtPlugin.log(e, "failed to refresh folders");
+ }
+
+ return;
+ }
+
+ /**
+ * Parse the output of llvm-rs-cc and mark the file with any errors.
+ * @param lines The output to parse.
+ * @return true if the parsing failed, false if success.
+ */
+ private boolean parseLlvmOutput(ArrayList<String> lines) {
+ // nothing to parse? just return false;
+ if (lines.size() == 0) {
+ return false;
+ }
+
+ // get the root folder for the project as we're going to ignore everything that's
+ // not in the project
+ String rootPath = mProject.getLocation().toOSString();
+ int rootPathLength = rootPath.length();
+
+ Matcher m;
+
+ boolean parsing = false;
+
+ for (int i = 0; i < lines.size(); i++) {
+ String p = lines.get(i);
+
+ m = sLlvmPattern1.matcher(p);
+ if (m.matches()) {
+ // get the file path. This may, or may not be the main file being compiled.
+ String filePath = m.group(1);
+ if (filePath.startsWith(rootPath) == false) {
+ // looks like the error in a non-project file. Keep parsing, but
+ // we'll return true
+ parsing = true;
+ continue;
+ }
+
+ // get the actual file.
+ filePath = filePath.substring(rootPathLength);
+ // remove starting separator since we want the path to be relative
+ if (filePath.startsWith(File.separator)) {
+ filePath = filePath.substring(1);
+ }
+
+ // get the file
+ IFile f = mProject.getFile(new Path(filePath));
+
+ String lineStr = m.group(2);
+ // ignore group 3 for now, this is the col number
+ String msg = m.group(4);
+
+ // get the line number
+ int line = 0;
+ try {
+ line = Integer.parseInt(lineStr);
+ } catch (NumberFormatException e) {
+ // looks like the string we extracted wasn't a valid
+ // file number. Parsing failed and we return true
+ return true;
+ }
+
+ // mark the file
+ BaseProjectHelper.markResource(f, AdtConstants.MARKER_RENDERSCRIPT, msg, line,
+ IMarker.SEVERITY_ERROR);
+
+ // success, go to the next line
+ continue;
+ }
+
+ // invalid line format, flag as error, and keep going
+ parsing = true;
+ }
+
+ return parsing;
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/RsSourceChangeHandler.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/RsSourceChangeHandler.java
new file mode 100644
index 000000000..715895a0c
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/RsSourceChangeHandler.java
@@ -0,0 +1,90 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.internal.build;
+
+import com.android.SdkConstants;
+import com.android.annotations.NonNull;
+import com.android.ide.eclipse.adt.AdtPlugin;
+import com.android.sdklib.build.RenderScriptChecker;
+
+import org.eclipse.core.resources.IFile;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.Set;
+
+public class RsSourceChangeHandler implements SourceChangeHandler {
+
+ private final RenderScriptChecker mChecker;
+ private boolean mIsCheckerLoaded = false;
+
+ private boolean mMustCompile = false;
+
+ public RsSourceChangeHandler(@NonNull RenderScriptChecker checker) {
+ mChecker = checker;
+ }
+
+ @Override
+ public boolean handleGeneratedFile(IFile file, int kind) {
+ if (mMustCompile) {
+ return false;
+ }
+
+ if (!mIsCheckerLoaded) {
+ try {
+ mChecker.loadDependencies();
+ } catch (IOException e) {
+ // failed to load the dependency files, force a compilation, log the error.
+ AdtPlugin.log(e, "Failed to read dependency files");
+ mMustCompile = true;
+ return false;
+ }
+ }
+
+ Set<File> oldOutputs = mChecker.getOldOutputs();
+ // mustCompile is always false here.
+ mMustCompile = oldOutputs.contains(file.getLocation().toFile());
+ return mMustCompile;
+ }
+
+ @Override
+ public void handleSourceFile(IFile file, int kind) {
+ if (mMustCompile) {
+ return;
+ }
+
+ String ext = file.getFileExtension();
+ if (SdkConstants.EXT_RS.equals(ext) ||
+ SdkConstants.EXT_FS.equals(ext) ||
+ SdkConstants.EXT_RSH.equals(ext)) {
+ mMustCompile = true;
+ }
+ }
+
+ public boolean mustCompile() {
+ return mMustCompile;
+ }
+
+ @NonNull
+ public RenderScriptChecker getChecker() {
+ return mChecker;
+ }
+
+ public void prepareFullBuild() {
+ mMustCompile = true;
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/SourceChangeHandler.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/SourceChangeHandler.java
new file mode 100644
index 000000000..12a055106
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/SourceChangeHandler.java
@@ -0,0 +1,25 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.internal.build;
+
+import org.eclipse.core.resources.IFile;
+
+public interface SourceChangeHandler {
+
+ boolean handleGeneratedFile(IFile file, int kind);
+ void handleSourceFile(IFile file, int kind);
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/SourceFileData.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/SourceFileData.java
new file mode 100644
index 000000000..d06bf1613
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/SourceFileData.java
@@ -0,0 +1,132 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.internal.build;
+
+import org.eclipse.core.resources.IFile;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Data for Android-specific source files. It contains a list of output files and a list
+ * of dependencies.
+ * The source file itself is a implied dependency and is not meant to be in the dependency list.
+ */
+public class SourceFileData {
+
+ private final IFile mSourceFile;
+ private final List<IFile> mOutputFiles = new ArrayList<IFile>();
+ private final List<IFile> mDependencyFiles = new ArrayList<IFile>();
+
+ public SourceFileData(IFile sourceFile) {
+ this(sourceFile, null, null);
+ }
+
+ SourceFileData(IFile sourceFile,
+ List<IFile> outputFiles, List<IFile> dependencyFiles) {
+ mSourceFile = sourceFile;
+ if (outputFiles != null) {
+ mOutputFiles.addAll(outputFiles);
+ }
+ if (dependencyFiles != null) {
+ mDependencyFiles.addAll(dependencyFiles);
+ }
+ }
+
+ SourceFileData(IFile sourceFile, IFile outputFile) {
+ mSourceFile = sourceFile;
+ if (outputFile != null) {
+ mOutputFiles.add(outputFile);
+ }
+ }
+
+ /**
+ * Returns the source file as an {@link IFile}
+ */
+ public IFile getSourceFile() {
+ return mSourceFile;
+ }
+
+ /**
+ * Returns whether the given file is a dependency for this source file.
+ * <p/>Note that the source file itself is not tested against. Therefore if
+ * {@code file.equals(getSourceFile()} returns {@code true}, this method will return
+ * {@code false}.
+ * @param file the file to check against
+ * @return true if the given file is a dependency for this source file.
+ */
+ public boolean dependsOn(IFile file) {
+ return mDependencyFiles.contains(file);
+ }
+
+ /**
+ * Returns whether the given file is an ouput of this source file.
+ * @param file the file to test.
+ * @return true if the file is an output file.
+ */
+ public boolean generated(IFile file) {
+ return mOutputFiles.contains(file);
+ }
+
+ void setOutputFiles(List<IFile> outputFiles) {
+ mOutputFiles.clear();
+ if (outputFiles != null) {
+ mOutputFiles.addAll(outputFiles);
+ }
+ }
+
+ void setOutputFile(IFile outputFile) {
+ mOutputFiles.clear();
+ if (outputFile != null) {
+ mOutputFiles.add(outputFile);
+ }
+ }
+
+ void setDependencyFiles(List<IFile> depFiles) {
+ mDependencyFiles.clear();
+ if (depFiles != null) {
+ mDependencyFiles.addAll(depFiles);
+ }
+ }
+
+ public List<IFile> getDependencyFiles() {
+ return mDependencyFiles;
+ }
+
+ /**
+ * Shortcut access to the first output file. This is useful for generator that only output
+ * one file.
+ */
+ public IFile getOutput() {
+ if (mOutputFiles.size() > 0) {
+ return mOutputFiles.get(0);
+ }
+
+ return null;
+ }
+
+ public List<IFile> getOutputFiles() {
+ return Collections.unmodifiableList(mOutputFiles);
+ }
+
+ @Override
+ public String toString() {
+ return "NonJavaFileBundle [mSourceFile=" + mSourceFile + ", mGeneratedFiles="
+ + mOutputFiles + ", mDependencies=" + mDependencyFiles + "]";
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/SourceProcessor.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/SourceProcessor.java
new file mode 100644
index 000000000..1fc3e4e53
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/SourceProcessor.java
@@ -0,0 +1,454 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.internal.build;
+
+import com.android.SdkConstants;
+import com.android.annotations.NonNull;
+import com.android.ide.eclipse.adt.internal.build.builders.BaseBuilder;
+import com.android.ide.eclipse.adt.internal.project.BaseProjectHelper;
+import com.android.ide.eclipse.adt.internal.project.ProjectHelper;
+import com.android.sdklib.BuildToolInfo;
+import com.android.sdklib.IAndroidTarget;
+
+import org.eclipse.core.resources.IFile;
+import org.eclipse.core.resources.IFolder;
+import org.eclipse.core.resources.IProject;
+import org.eclipse.core.resources.IResource;
+import org.eclipse.core.resources.IWorkspaceRoot;
+import org.eclipse.core.resources.ResourcesPlugin;
+import org.eclipse.core.runtime.CoreException;
+import org.eclipse.core.runtime.IPath;
+import org.eclipse.core.runtime.IProgressMonitor;
+import org.eclipse.jdt.core.IJavaProject;
+
+import java.io.File;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Base class to handle generated java code.
+ *
+ * It provides management for modified source file list, deleted source file list, reconciliation
+ * of previous lists, storing the current state of the build.
+ *
+ */
+public abstract class SourceProcessor {
+
+ public final static int COMPILE_STATUS_NONE = 0;
+ public final static int COMPILE_STATUS_CODE = 0x1;
+ public final static int COMPILE_STATUS_RES = 0x2;
+
+ /** List of all source files, their dependencies, and their output. */
+ private final Map<IFile, SourceFileData> mFiles = new HashMap<IFile, SourceFileData>();
+
+ private final IJavaProject mJavaProject;
+ private BuildToolInfo mBuildToolInfo;
+ private final IFolder mGenFolder;
+ private final DefaultSourceChangeHandler mDeltaVisitor;
+
+ /** List of source files pending compilation at the next build */
+ private final List<IFile> mToCompile = new ArrayList<IFile>();
+
+ /** List of removed source files pending cleaning at the next build. */
+ private final List<IFile> mRemoved = new ArrayList<IFile>();
+
+ private int mLastCompilationStatus = COMPILE_STATUS_NONE;
+
+ /**
+ * Quotes a path inside "". If the platform is not windows, the path is returned as is.
+ * @param path the path to quote
+ * @return the quoted path.
+ */
+ public static String quote(String path) {
+ if (SdkConstants.CURRENT_PLATFORM == SdkConstants.PLATFORM_WINDOWS) {
+ if (path.endsWith(File.separator)) {
+ path = path.substring(0, path.length() -1);
+ }
+ return "\"" + path + "\"";
+ }
+
+ return path;
+ }
+
+ protected SourceProcessor(
+ @NonNull IJavaProject javaProject,
+ @NonNull BuildToolInfo buildToolInfo,
+ @NonNull IFolder genFolder,
+ @NonNull DefaultSourceChangeHandler deltaVisitor) {
+ mJavaProject = javaProject;
+ mBuildToolInfo = buildToolInfo;
+ mGenFolder = genFolder;
+ mDeltaVisitor = deltaVisitor;
+
+ mDeltaVisitor.init(this);
+
+ IProject project = javaProject.getProject();
+
+ // get all the source files
+ buildSourceFileList();
+
+ // load the known dependencies
+ loadOutputAndDependencies();
+
+ boolean mustCompile = loadState(project);
+
+ // if we stored that we have to compile some files, we build the list that will compile them
+ // all. For now we have to reuse the full list since we don't know which files needed
+ // compilation.
+ if (mustCompile) {
+ mToCompile.addAll(mFiles.keySet());
+ }
+ }
+
+ protected SourceProcessor(
+ @NonNull IJavaProject javaProject,
+ @NonNull BuildToolInfo buildToolInfo,
+ @NonNull IFolder genFolder) {
+ this(javaProject, buildToolInfo, genFolder, new DefaultSourceChangeHandler());
+ }
+
+ public void setBuildToolInfo(BuildToolInfo buildToolInfo) {
+ mBuildToolInfo = buildToolInfo;
+ }
+
+
+ /**
+ * Returns whether the given file is an output of this processor by return the source
+ * file that generated it.
+ * @param file the file to test.
+ * @return the source file that generated the given file or null.
+ */
+ IFile isOutput(IFile file) {
+ for (SourceFileData data : mFiles.values()) {
+ if (data.generated(file)) {
+ return data.getSourceFile();
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Returns whether the given file is a dependency for other files by returning a list
+ * of file depending on the given file.
+ * @param file the file to test.
+ * @return a list of files that depend on the given file or an empty list if there
+ * are no matches.
+ */
+ List<IFile> isDependency(IFile file) {
+ ArrayList<IFile> files = new ArrayList<IFile>();
+ for (SourceFileData data : mFiles.values()) {
+ if (data.dependsOn(file)) {
+ files.add(data.getSourceFile());
+ }
+ }
+
+ return files;
+ }
+
+ void addData(SourceFileData data) {
+ mFiles.put(data.getSourceFile(), data);
+ }
+
+ SourceFileData getFileData(IFile file) {
+ return mFiles.get(file);
+ }
+
+ Collection<SourceFileData> getAllFileData() {
+ return mFiles.values();
+ }
+
+ public final DefaultSourceChangeHandler getChangeHandler() {
+ return mDeltaVisitor;
+ }
+
+ final IJavaProject getJavaProject() {
+ return mJavaProject;
+ }
+
+ final BuildToolInfo getBuildToolInfo() {
+ return mBuildToolInfo;
+ }
+
+ final IFolder getGenFolder() {
+ return mGenFolder;
+ }
+
+ final List<IFile> getToCompile() {
+ return mToCompile;
+ }
+
+ final List<IFile> getRemovedFile() {
+ return mRemoved;
+ }
+
+ final void addFileToCompile(IFile file) {
+ mToCompile.add(file);
+ }
+
+ public final void prepareFullBuild(IProject project) {
+ mDeltaVisitor.reset();
+
+ mToCompile.clear();
+ mRemoved.clear();
+
+ // get all the source files
+ buildSourceFileList();
+
+ mToCompile.addAll(mFiles.keySet());
+
+ saveState(project);
+ }
+
+ public final void doneVisiting(IProject project) {
+ // merge the previous file modification lists and the new one.
+ mergeFileModifications(mDeltaVisitor);
+
+ mDeltaVisitor.reset();
+
+ saveState(project);
+ }
+
+ /**
+ * Returns the extension of the source files handled by this processor.
+ * @return
+ */
+ protected abstract Set<String> getExtensions();
+
+ protected abstract String getSavePropertyName();
+
+ /**
+ * Compiles the source files and return a status bitmask of the type of file that was generated.
+ *
+ */
+ public final int compileFiles(BaseBuilder builder,
+ IProject project, IAndroidTarget projectTarget,
+ List<IPath> sourceFolders, List<File> libraryProjectsOut, IProgressMonitor monitor)
+ throws CoreException {
+
+ mLastCompilationStatus = COMPILE_STATUS_NONE;
+
+ if (mToCompile.size() == 0 && mRemoved.size() == 0) {
+ return mLastCompilationStatus;
+ }
+
+ // if a source file is being removed before we managed to compile it, it'll be in
+ // both list. We *need* to remove it from the compile list or it'll never go away.
+ for (IFile sourceFile : mRemoved) {
+ int pos = mToCompile.indexOf(sourceFile);
+ if (pos != -1) {
+ mToCompile.remove(pos);
+ }
+ }
+
+ // list of files that have failed compilation.
+ List<IFile> stillNeedCompilation = new ArrayList<IFile>();
+
+ doCompileFiles(mToCompile, builder, project, projectTarget, sourceFolders,
+ stillNeedCompilation, libraryProjectsOut, monitor);
+
+ mToCompile.clear();
+ mToCompile.addAll(stillNeedCompilation);
+
+ // Remove the files created from source files that have been removed.
+ for (IFile sourceFile : mRemoved) {
+ // look if we already know the output
+ SourceFileData data = getFileData(sourceFile);
+ if (data != null) {
+ doRemoveFiles(data);
+ }
+ }
+
+ // remove the associated file data.
+ for (IFile removedFile : mRemoved) {
+ mFiles.remove(removedFile);
+ }
+
+ mRemoved.clear();
+
+ // store the build state. If there are any files that failed to compile, we will
+ // force a full aidl compile on the next project open. (unless a full compilation succeed
+ // before the project is closed/re-opened.)
+ saveState(project);
+
+ return mLastCompilationStatus;
+ }
+
+ protected abstract void doCompileFiles(
+ List<IFile> filesToCompile, BaseBuilder builder,
+ IProject project, IAndroidTarget projectTarget,
+ List<IPath> sourceFolders, List<IFile> notCompiledOut,
+ List<File> libraryProjectsOut, IProgressMonitor monitor) throws CoreException;
+
+ /**
+ * Adds a compilation status. It can be any of (in combination too):
+ * <p/>
+ * {@link #COMPILE_STATUS_CODE} means this processor created source code files.
+ * {@link #COMPILE_STATUS_RES} means this process created resources.
+ */
+ protected void setCompilationStatus(int status) {
+ mLastCompilationStatus |= status;
+ }
+
+ protected void doRemoveFiles(SourceFileData data) throws CoreException {
+ List<IFile> outputFiles = data.getOutputFiles();
+ for (IFile outputFile : outputFiles) {
+ if (outputFile.exists()) {
+ outputFile.getLocation().toFile().delete();
+ }
+ }
+ }
+
+ public final boolean loadState(IProject project) {
+ return ProjectHelper.loadBooleanProperty(project, getSavePropertyName(),
+ true /*defaultValue*/);
+ }
+
+ public final void saveState(IProject project) {
+ // TODO: Optimize by saving only the files that need compilation
+ ProjectHelper.saveStringProperty(project, getSavePropertyName(),
+ Boolean.toString(mToCompile.size() > 0));
+ }
+
+ protected abstract void loadOutputAndDependencies();
+
+
+ protected IPath getSourceFolderFor(IFile file) {
+ // find the source folder for the class so that we can infer the package from the
+ // difference between the file and its source folder.
+ List<IPath> sourceFolders = BaseProjectHelper.getSourceClasspaths(getJavaProject());
+ IWorkspaceRoot root = ResourcesPlugin.getWorkspace().getRoot();
+
+ for (IPath sourceFolderPath : sourceFolders) {
+ IFolder sourceFolder = root.getFolder(sourceFolderPath);
+ // we don't look in the 'gen' source folder as there will be no source in there.
+ if (sourceFolder.exists() && sourceFolder.equals(getGenFolder()) == false) {
+ // look for the source file parent, until we find this source folder.
+ IResource parent = file;
+ while ((parent = parent.getParent()) != null) {
+ if (parent.equals(sourceFolder)) {
+ return sourceFolderPath;
+ }
+ }
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Goes through the build paths and fills the list of files to compile.
+ *
+ * @param project The project.
+ * @param sourceFolderPathList The list of source folder paths.
+ */
+ private final void buildSourceFileList() {
+ mFiles.clear();
+
+ IWorkspaceRoot root = ResourcesPlugin.getWorkspace().getRoot();
+ List<IPath> sourceFolderPathList = BaseProjectHelper.getSourceClasspaths(mJavaProject);
+
+ for (IPath sourceFolderPath : sourceFolderPathList) {
+ IFolder sourceFolder = root.getFolder(sourceFolderPath);
+ // we don't look in the 'gen' source folder as there will be no source in there.
+ if (sourceFolder.exists() && sourceFolder.equals(getGenFolder()) == false) {
+ scanFolderForSourceFiles(sourceFolder, sourceFolder);
+ }
+ }
+ }
+
+ /**
+ * Scans a folder and fills the list of files to compile.
+ * @param sourceFolder the root source folder.
+ * @param folder The folder to scan.
+ */
+ private void scanFolderForSourceFiles(IFolder sourceFolder, IFolder folder) {
+ try {
+ IResource[] members = folder.members();
+ for (IResource r : members) {
+ // get the type of the resource
+ switch (r.getType()) {
+ case IResource.FILE: {
+ // if this a file, check that the file actually exist
+ // and that it's the type of of file that's used in this processor
+ String extension = r.exists() ? r.getFileExtension() : null;
+ if (extension != null &&
+ getExtensions().contains(extension.toLowerCase(Locale.US))) {
+ mFiles.put((IFile) r, new SourceFileData((IFile) r));
+ }
+ break;
+ }
+ case IResource.FOLDER:
+ // recursively go through children
+ scanFolderForSourceFiles(sourceFolder, (IFolder)r);
+ break;
+ default:
+ // this would mean it's a project or the workspace root
+ // which is unlikely to happen. we do nothing
+ break;
+ }
+ }
+ } catch (CoreException e) {
+ // Couldn't get the members list for some reason. Just return.
+ }
+ }
+
+
+ /**
+ * Merge the current list of source file to compile/remove with the one coming from the
+ * delta visitor
+ * @param visitor the delta visitor.
+ */
+ private void mergeFileModifications(DefaultSourceChangeHandler visitor) {
+ Set<IFile> toRemove = visitor.getRemovedFiles();
+ Set<IFile> toCompile = visitor.getFilesToCompile();
+
+ // loop through the new toRemove list, and add it to the old one,
+ // plus remove any file that was still to compile and that are now
+ // removed
+ for (IFile r : toRemove) {
+ if (mRemoved.indexOf(r) == -1) {
+ mRemoved.add(r);
+ }
+
+ int index = mToCompile.indexOf(r);
+ if (index != -1) {
+ mToCompile.remove(index);
+ }
+ }
+
+ // now loop through the new files to compile and add it to the list.
+ // Also look for them in the remove list, this would mean that they
+ // were removed, then added back, and we shouldn't remove them, just
+ // recompile them.
+ for (IFile r : toCompile) {
+ if (mToCompile.indexOf(r) == -1) {
+ mToCompile.add(r);
+ }
+
+ int index = mRemoved.indexOf(r);
+ if (index != -1) {
+ mRemoved.remove(index);
+ }
+ }
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/build_messages.properties b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/build_messages.properties
new file mode 100644
index 000000000..f387ab5a9
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/build_messages.properties
@@ -0,0 +1,67 @@
+Start_Full_Apk_Build=Starting full Package build.
+Start_Full_Pre_Compiler=Starting full Pre Compiler.
+Skip_Post_Compiler=Skipping over Post Compiler.
+Start_Full_Post_Compiler=Starting full Post Compiler.
+Start_Inc_Apk_Build=Starting incremental Package build: Checking resource changes.
+Start_Inc_Pre_Compiler=Starting incremental Pre Compiler: Checking resource changes.
+Xml_Error=Error in an XML file: aborting build.
+s_Missing_Repackaging=%1$s missing. Repackaging.
+Project_Has_Errors=Project contains error(s). Package Builder aborted.
+Failed_To_Get_Output=Failed to get project output folder\!
+Output_Missing=Output folder missing\! Make sure your project is configured properly.
+s_File_Missing=%1$s file missing\!
+Unparsed_AAPT_Errors=Unparsed aapt error(s)\! Check the console for output.
+Unparsed_AIDL_Errors=Unparsed aidl error\! Check the console for output.
+AAPT_Exec_Error_s=Error executing aapt. Please check aapt is present at %1$s
+AAPT_Exec_Error_d=Error executing aapt: Return code %1$d
+AAPT_Exec_Error_Other_s=Error executing aapt: %1$s
+Dalvik_Error_d=Conversion to Dalvik format failed with error %1$d
+DX_Jar_Error=Dx.jar is not found inside the plugin. Reinstall ADT\!
+Dalvik_Error_s=Conversion to Dalvik format failed: %1$s
+Incompatible_VM_Warning=Note: You may be using an incompatible virtual machine or class library.
+Requires_1_5_Error=This program requires JDK 1.5 compatibility.
+Final_Archive_Error_s=Error generating final archive: %1$s
+Marker_Delete_Error=Failed to delete marker '%1$s' for %2$s
+Couldnt_Locate_s_Error=Could not locate '%1$s'. This will not be added to the package.
+Compiler_Compliance_Error=Compiler compliance level not compatible: Build aborted.
+No_SDK_Setup_Error=SDK directory has not been setup. Please go to the Android preferences and enter the location of the SDK.
+s_Contains_Xml_Error=%1$s contains XML error: Build aborted.
+s_Doesnt_Declare_Package_Error=%1$s does not declare a Java package: Build aborted.
+Checking_Package_Change=Checking Java package value did not change...
+Package_s_Doesnt_Exist_Error=Package '%1$s' does not exist\!
+Preparing_Generated_Files=Preparing generated java files for update/creation.
+AAPT_Error='aapt' error. Pre Compiler Build aborted.
+Nothing_To_Compile=Nothing to pre compile\!
+Removing_Generated_Classes=Removing generated java classes.
+Delete_Obsolete_Error=Failed to delete obsolete %1$s, please delete it manually
+DexWrapper_Dex_Loader=Dex Loader
+AIDL_Java_Conflict=%1$s is in the way of %2$s, remove it or rename of one the files.
+AIDL_Exec_Error_d=Error executing aidl: Return code %1$d
+AIDL_Exec_Error_s=Error executing aidl. Please check aidl is present at %1$s
+s_Removed_Recreating_s=%1$s was removed\! Recreating %1$s\!
+s_Modified_Manually_Recreating_s=%1$s was modified manually\! Reverting to generated version\!
+s_Modified_Recreating_s='%1$s' was modified.
+Added_s_s_Needs_Updating=New resource file: '%1$s', %2$s needs to be updated.
+s_Removed_s_Needs_Updating='%1$s' was removed, %2$s needs to be updated.
+Requires_Compiler_Compliance_s=Android requires compiler compliance level 5.0 or 6.0. Found '%1$s' instead. Please use Android Tools > Fix Project Properties.
+Requires_Source_Compatibility_s=Android requires source compatibility set to 5.0 or 6.0. Found '%1$s' instead. Please use Android Tools > Fix Project Properties.
+Requires_Class_Compatibility_s=Android requires .class compatibility set to 5.0 or 6.0. Found '%1$s' instead. Please use Android Tools > Fix Project Properties.
+Refreshing_Res=Refreshing resource folders.
+DexWrapper_s_does_not_exists=%1$s does not exist or is not a file
+DexWrapper_Failed_to_load_s=Failed to load %1$s
+DexWrapper_Unable_To_Execute_Dex_s=Unable to execute dex: %1$s
+DexWrapper_SecuryEx_Unable_To_Find_API=SecurityException: Unable to find API for dex.jar
+DexWrapper_SecuryEx_Unable_To_Find_Method=SecurityException: Unable to find method for dex.jar
+DexWrapper_SecuryEx_Unable_To_Find_Field=SecurityException: Unable to find field for dex.jar
+ApkBuilder_UnableBuild_Dex_Not_loaded=Unable to build: the file dx.jar was not loaded from the SDK folder\!
+ApkBuilder_Using_Default_Key=Using default debug key to sign package
+ApkBuilder_Using_s_To_Sign=Using '%1$s' to sign package
+ApkBuilder_Signing_Key_Creation_s=Signing Key Creation:
+ApkBuilder_Unable_To_Gey_Key=Unable to get debug signature key
+ApkBuilder_Certificate_Expired_on_s=Debug certificate expired on %1$s\!
+ApkBuilder_Packaging_s=Packaging %1$s
+ApkBuilder_JAVA_HOME_is_s=The Java VM Home used is: %1$s
+ApkBuilder_Update_or_Execute_manually_s=Update it if necessary, or manually execute the following command:
+ApkBuilder_s_Conflict_with_file_s=%1$s conflicts with another file already put at %2$s
+ApkBuilder_Packaging_s_into_s=Packaging %1$s into %2$s
+Proguard_Exec_Error=Error executing Proguard. Please check Proguard is present at %1$s
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/builders/BaseBuilder.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/builders/BaseBuilder.java
new file mode 100644
index 000000000..162591406
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/builders/BaseBuilder.java
@@ -0,0 +1,483 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.internal.build.builders;
+
+import com.android.annotations.NonNull;
+import com.android.annotations.Nullable;
+import com.android.ide.common.sdk.LoadStatus;
+import com.android.ide.eclipse.adt.AdtConstants;
+import com.android.ide.eclipse.adt.AdtPlugin;
+import com.android.ide.eclipse.adt.internal.build.BuildHelper;
+import com.android.ide.eclipse.adt.internal.build.Messages;
+import com.android.ide.eclipse.adt.internal.preferences.AdtPrefs.BuildVerbosity;
+import com.android.ide.eclipse.adt.internal.project.BaseProjectHelper;
+import com.android.ide.eclipse.adt.internal.project.ProjectHelper;
+import com.android.ide.eclipse.adt.internal.project.XmlErrorHandler;
+import com.android.ide.eclipse.adt.internal.project.XmlErrorHandler.XmlErrorListener;
+import com.android.ide.eclipse.adt.internal.sdk.ProjectState;
+import com.android.ide.eclipse.adt.internal.sdk.Sdk;
+import com.android.ide.eclipse.adt.io.IFileWrapper;
+import com.android.io.IAbstractFile;
+import com.android.io.StreamException;
+import com.android.sdklib.BuildToolInfo;
+import com.android.sdklib.IAndroidTarget;
+import com.android.sdklib.repository.FullRevision;
+
+import org.eclipse.core.resources.IContainer;
+import org.eclipse.core.resources.IFile;
+import org.eclipse.core.resources.IFolder;
+import org.eclipse.core.resources.IMarker;
+import org.eclipse.core.resources.IProject;
+import org.eclipse.core.resources.IResource;
+import org.eclipse.core.resources.IncrementalProjectBuilder;
+import org.eclipse.core.resources.ResourcesPlugin;
+import org.eclipse.core.runtime.CoreException;
+import org.eclipse.core.runtime.IPath;
+import org.eclipse.core.runtime.IProgressMonitor;
+import org.eclipse.core.runtime.jobs.Job;
+import org.eclipse.jdt.core.IJavaProject;
+import org.xml.sax.SAXException;
+
+import java.util.ArrayList;
+
+import javax.xml.parsers.ParserConfigurationException;
+import javax.xml.parsers.SAXParser;
+import javax.xml.parsers.SAXParserFactory;
+
+/**
+ * Base builder for XML files. This class allows for basic XML parsing with
+ * error checking and marking the files for errors/warnings.
+ */
+public abstract class BaseBuilder extends IncrementalProjectBuilder {
+
+ protected static final boolean DEBUG_LOG = "1".equals( //$NON-NLS-1$
+ System.getenv("ANDROID_BUILD_DEBUG")); //$NON-NLS-1$
+
+ /** SAX Parser factory. */
+ private SAXParserFactory mParserFactory;
+
+ /**
+ * The build tool to use to build. This is guaranteed to be non null after a call to
+ * {@link #abortOnBadSetup(IJavaProject, ProjectState)} since this will throw if it can't be
+ * queried.
+ */
+ protected BuildToolInfo mBuildToolInfo;
+
+ /**
+ * Base Resource Delta Visitor to handle XML error
+ */
+ protected static class BaseDeltaVisitor implements XmlErrorListener {
+
+ /** The Xml builder used to validate XML correctness. */
+ protected BaseBuilder mBuilder;
+
+ /**
+ * XML error flag. if true, we keep parsing the ResourceDelta but the
+ * compilation will not happen (we're putting markers)
+ */
+ public boolean mXmlError = false;
+
+ public BaseDeltaVisitor(BaseBuilder builder) {
+ mBuilder = builder;
+ }
+
+ /**
+ * Finds a matching Source folder for the current path. This checks if the current path
+ * leads to, or is a source folder.
+ * @param sourceFolders The list of source folders
+ * @param pathSegments The segments of the current path
+ * @return The segments of the source folder, or null if no match was found
+ */
+ protected static String[] findMatchingSourceFolder(ArrayList<IPath> sourceFolders,
+ String[] pathSegments) {
+
+ for (IPath p : sourceFolders) {
+ // check if we are inside one of those source class path
+
+ // get the segments
+ String[] srcSegments = p.segments();
+
+ // compare segments. We want the path of the resource
+ // we're visiting to be
+ boolean valid = true;
+ int segmentCount = pathSegments.length;
+
+ for (int i = 0 ; i < segmentCount; i++) {
+ String s1 = pathSegments[i];
+ String s2 = srcSegments[i];
+
+ if (s1.equalsIgnoreCase(s2) == false) {
+ valid = false;
+ break;
+ }
+ }
+
+ if (valid) {
+ // this folder, or one of this children is a source
+ // folder!
+ // we return its segments
+ return srcSegments;
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Sent when an XML error is detected.
+ * @see XmlErrorListener
+ */
+ @Override
+ public void errorFound() {
+ mXmlError = true;
+ }
+ }
+
+ protected static class AbortBuildException extends Exception {
+ private static final long serialVersionUID = 1L;
+ }
+
+ public BaseBuilder() {
+ super();
+ mParserFactory = SAXParserFactory.newInstance();
+
+ // FIXME when the compiled XML support for namespace is in, set this to true.
+ mParserFactory.setNamespaceAware(false);
+ }
+
+ /**
+ * Checks an Xml file for validity. Errors/warnings will be marked on the
+ * file
+ * @param resource the resource to check
+ * @param visitor a valid resource delta visitor
+ */
+ protected final void checkXML(IResource resource, BaseDeltaVisitor visitor) {
+
+ // first make sure this is an xml file
+ if (resource instanceof IFile) {
+ IFile file = (IFile)resource;
+
+ // remove previous markers
+ removeMarkersFromResource(file, AdtConstants.MARKER_XML);
+
+ // create the error handler
+ XmlErrorHandler reporter = new XmlErrorHandler(file, visitor);
+ try {
+ // parse
+ getParser().parse(file.getContents(), reporter);
+ } catch (Exception e1) {
+ }
+ }
+ }
+
+ /**
+ * Returns the SAXParserFactory, instantiating it first if it's not already
+ * created.
+ * @return the SAXParserFactory object
+ * @throws ParserConfigurationException
+ * @throws SAXException
+ */
+ protected final SAXParser getParser() throws ParserConfigurationException,
+ SAXException {
+ return mParserFactory.newSAXParser();
+ }
+
+ /**
+ * Adds a marker to the current project. This methods catches thrown {@link CoreException},
+ * and returns null instead.
+ *
+ * @param markerId The id of the marker to add.
+ * @param message the message associated with the mark
+ * @param severity the severity of the marker.
+ * @return the marker that was created (or null if failure)
+ * @see IMarker
+ */
+ protected final IMarker markProject(String markerId, String message, int severity) {
+ return BaseProjectHelper.markResource(getProject(), markerId, message, severity);
+ }
+
+ /**
+ * Removes markers from a resource and only the resource (not its children).
+ * @param file The file from which to delete the markers.
+ * @param markerId The id of the markers to remove. If null, all marker of
+ * type <code>IMarker.PROBLEM</code> will be removed.
+ */
+ public final void removeMarkersFromResource(IResource resource, String markerId) {
+ try {
+ if (resource.exists()) {
+ resource.deleteMarkers(markerId, true, IResource.DEPTH_ZERO);
+ }
+ } catch (CoreException ce) {
+ String msg = String.format(Messages.Marker_Delete_Error, markerId, resource.toString());
+ AdtPlugin.printErrorToConsole(getProject(), msg);
+ }
+ }
+
+ /**
+ * Removes markers from a container and its children.
+ * @param folder The container from which to delete the markers.
+ * @param markerId The id of the markers to remove. If null, all marker of
+ * type <code>IMarker.PROBLEM</code> will be removed.
+ */
+ protected final void removeMarkersFromContainer(IContainer folder, String markerId) {
+ try {
+ if (folder.exists()) {
+ folder.deleteMarkers(markerId, true, IResource.DEPTH_INFINITE);
+ }
+ } catch (CoreException ce) {
+ String msg = String.format(Messages.Marker_Delete_Error, markerId, folder.toString());
+ AdtPlugin.printErrorToConsole(getProject(), msg);
+ }
+ }
+
+ /**
+ * Get the stderr output of a process and return when the process is done.
+ * @param process The process to get the ouput from
+ * @param stdErr The array to store the stderr output
+ * @return the process return code.
+ * @throws InterruptedException
+ */
+ protected final int grabProcessOutput(final Process process,
+ final ArrayList<String> stdErr) throws InterruptedException {
+ return BuildHelper.grabProcessOutput(getProject(), process, stdErr);
+ }
+
+
+
+ /**
+ * Saves a String property into the persistent storage of the project.
+ * @param propertyName the name of the property. The id of the plugin is added to this string.
+ * @param value the value to save
+ * @return true if the save succeeded.
+ */
+ protected boolean saveProjectStringProperty(String propertyName, String value) {
+ IProject project = getProject();
+ return ProjectHelper.saveStringProperty(project, propertyName, value);
+ }
+
+
+ /**
+ * Loads a String property from the persistent storage of the project.
+ * @param propertyName the name of the property. The id of the plugin is added to this string.
+ * @return the property value or null if it was not found.
+ */
+ protected String loadProjectStringProperty(String propertyName) {
+ IProject project = getProject();
+ return ProjectHelper.loadStringProperty(project, propertyName);
+ }
+
+ /**
+ * Saves a property into the persistent storage of the project.
+ * @param propertyName the name of the property. The id of the plugin is added to this string.
+ * @param value the value to save
+ * @return true if the save succeeded.
+ */
+ protected boolean saveProjectBooleanProperty(String propertyName, boolean value) {
+ IProject project = getProject();
+ return ProjectHelper.saveStringProperty(project, propertyName, Boolean.toString(value));
+ }
+
+ /**
+ * Loads a boolean property from the persistent storage of the project.
+ * @param propertyName the name of the property. The id of the plugin is added to this string.
+ * @param defaultValue The default value to return if the property was not found.
+ * @return the property value or the default value if the property was not found.
+ */
+ protected boolean loadProjectBooleanProperty(String propertyName, boolean defaultValue) {
+ IProject project = getProject();
+ return ProjectHelper.loadBooleanProperty(project, propertyName, defaultValue);
+ }
+
+ /**
+ * Aborts the build if the SDK/project setups are broken. This does not
+ * display any errors.
+ *
+ * @param javaProject The {@link IJavaProject} being compiled.
+ * @param projectState the project state, optional. will be queried if null.
+ * @throws CoreException
+ */
+ protected void abortOnBadSetup(@NonNull IJavaProject javaProject,
+ @Nullable ProjectState projectState) throws AbortBuildException, CoreException {
+ IProject iProject = javaProject.getProject();
+ // check if we have finished loading the project target.
+ Sdk sdk = Sdk.getCurrent();
+ if (sdk == null) {
+ throw new AbortBuildException();
+ }
+
+ if (projectState == null) {
+ projectState = Sdk.getProjectState(javaProject.getProject());
+ }
+
+ // get the target for the project
+ IAndroidTarget target = projectState.getTarget();
+
+ if (target == null) {
+ throw new AbortBuildException();
+ }
+
+ // check on the target data.
+ if (sdk.checkAndLoadTargetData(target, javaProject) != LoadStatus.LOADED) {
+ throw new AbortBuildException();
+ }
+
+ mBuildToolInfo = projectState.getBuildToolInfo();
+ if (mBuildToolInfo == null) {
+ mBuildToolInfo = sdk.getLatestBuildTool();
+
+ if (mBuildToolInfo == null) {
+ AdtPlugin.printBuildToConsole(BuildVerbosity.VERBOSE, iProject,
+ "No \"Build Tools\" package available; use SDK Manager to install one.");
+ throw new AbortBuildException();
+ } else {
+ AdtPlugin.printBuildToConsole(BuildVerbosity.VERBOSE, iProject,
+ String.format("Using default Build Tools revision %s",
+ mBuildToolInfo.getRevision())
+ );
+ }
+ }
+
+ // abort if there are TARGET or ADT type markers
+ stopOnMarker(iProject, AdtConstants.MARKER_TARGET, IResource.DEPTH_ZERO,
+ false /*checkSeverity*/);
+ stopOnMarker(iProject, AdtConstants.MARKER_ADT, IResource.DEPTH_ZERO,
+ false /*checkSeverity*/);
+ }
+
+ protected void stopOnMarker(IProject project, String markerType, int depth,
+ boolean checkSeverity)
+ throws AbortBuildException {
+ try {
+ IMarker[] markers = project.findMarkers(markerType, false /*includeSubtypes*/, depth);
+
+ if (markers.length > 0) {
+ if (checkSeverity == false) {
+ throw new AbortBuildException();
+ } else {
+ for (IMarker marker : markers) {
+ int severity = marker.getAttribute(IMarker.SEVERITY, -1 /*defaultValue*/);
+ if (severity == IMarker.SEVERITY_ERROR) {
+ throw new AbortBuildException();
+ }
+ }
+ }
+ }
+ } catch (CoreException e) {
+ // don't stop, something's really screwed up and the build will break later with
+ // a better error message.
+ }
+ }
+
+ /**
+ * Handles a {@link StreamException} by logging the info and marking the project.
+ * This should generally be followed by exiting the build process.
+ *
+ * @param e the exception
+ */
+ protected void handleStreamException(StreamException e) {
+ IAbstractFile file = e.getFile();
+
+ String msg;
+
+ IResource target = getProject();
+ if (file instanceof IFileWrapper) {
+ target = ((IFileWrapper) file).getIFile();
+
+ if (e.getError() == StreamException.Error.OUTOFSYNC) {
+ msg = "File is Out of sync";
+ } else {
+ msg = "Error reading file. Read log for details";
+ }
+
+ } else {
+ if (e.getError() == StreamException.Error.OUTOFSYNC) {
+ msg = String.format("Out of sync file: %s", file.getOsLocation());
+ } else {
+ msg = String.format("Error reading file %s. Read log for details",
+ file.getOsLocation());
+ }
+ }
+
+ AdtPlugin.logAndPrintError(e, getProject().getName(), msg);
+ BaseProjectHelper.markResource(target, AdtConstants.MARKER_ADT, msg,
+ IMarker.SEVERITY_ERROR);
+ }
+
+ /**
+ * Handles a generic {@link Throwable} by logging the info and marking the project.
+ * This should generally be followed by exiting the build process.
+ *
+ * @param t the {@link Throwable}.
+ * @param message the message to log and to associate with the marker.
+ */
+ protected void handleException(Throwable t, String message) {
+ AdtPlugin.logAndPrintError(t, getProject().getName(), message);
+ markProject(AdtConstants.MARKER_ADT, message, IMarker.SEVERITY_ERROR);
+ }
+
+ /**
+ * Recursively delete all the derived resources from a root resource. The root resource is not
+ * deleted.
+ * @param rootResource the root resource
+ * @param monitor a progress monitor.
+ * @throws CoreException
+ *
+ */
+ protected void removeDerivedResources(IResource rootResource, IProgressMonitor monitor)
+ throws CoreException {
+ removeDerivedResources(rootResource, false, monitor);
+ }
+
+ /**
+ * delete a resource and its children. returns true if the root resource was deleted. All
+ * sub-folders *will* be deleted if they were emptied (not if they started empty).
+ * @param rootResource the root resource
+ * @param deleteRoot whether to delete the root folder.
+ * @param monitor a progress monitor.
+ * @throws CoreException
+ */
+ private void removeDerivedResources(IResource rootResource, boolean deleteRoot,
+ IProgressMonitor monitor) throws CoreException {
+ if (rootResource.exists()) {
+ // if it's a folder, delete derived member.
+ if (rootResource.getType() == IResource.FOLDER) {
+ IFolder folder = (IFolder)rootResource;
+ IResource[] members = folder.members();
+ boolean wasNotEmpty = members.length > 0;
+ for (IResource member : members) {
+ removeDerivedResources(member, true /*deleteRoot*/, monitor);
+ }
+
+ // if the folder had content that is now all removed, delete the folder.
+ if (deleteRoot && wasNotEmpty && folder.members().length == 0) {
+ rootResource.getLocation().toFile().delete();
+ }
+ }
+
+ // if the root resource is derived, delete it.
+ if (rootResource.isDerived()) {
+ rootResource.getLocation().toFile().delete();
+ }
+ }
+ }
+
+ protected void launchJob(Job newJob) {
+ newJob.setPriority(Job.BUILD);
+ newJob.setRule(ResourcesPlugin.getWorkspace().getRoot());
+ newJob.schedule();
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/builders/ChangedFileSet.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/builders/ChangedFileSet.java
new file mode 100644
index 000000000..4f5b47f6d
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/builders/ChangedFileSet.java
@@ -0,0 +1,70 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.internal.build.builders;
+
+import com.android.annotations.NonNull;
+
+import org.apache.tools.ant.types.selectors.SelectorUtils;
+import org.eclipse.core.runtime.IPath;
+
+/**
+ * Collection of file path or path patterns to be checked for changes.
+ *
+ * All paths should be relative to the project they belong to.
+ * Patterns can use Ant-type glob patterns.
+ *
+ * This is an immutable class that does not store any info beyond the list of paths. This is to
+ * be used in conjunction with {@link PatternBasedDeltaVisitor}.
+ */
+class ChangedFileSet {
+
+ private final String mLogName;
+
+ private final String[] mInputs;
+ private String mOutput;
+
+ ChangedFileSet(String logName, String... inputs) {
+ mLogName = logName;
+ mInputs = inputs;
+ }
+
+ public void setOutput(@NonNull String output) {
+ mOutput = output;
+ }
+
+ public boolean isInput(@NonNull String path, @NonNull IPath iPath) {
+ for (String i : mInputs) {
+ if (SelectorUtils.matchPath(i, path)) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ public boolean isOutput(@NonNull String path, @NonNull IPath iPath) {
+ if (mOutput != null) {
+ return SelectorUtils.matchPath(mOutput, path);
+ }
+
+ return false;
+ }
+
+ public String getLogName() {
+ return mLogName;
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/builders/ChangedFileSetHelper.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/builders/ChangedFileSetHelper.java
new file mode 100644
index 000000000..9fc19a7a6
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/builders/ChangedFileSetHelper.java
@@ -0,0 +1,200 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.internal.build.builders;
+
+import com.android.SdkConstants;
+import com.android.annotations.NonNull;
+import com.android.ide.eclipse.adt.AdtConstants;
+import com.android.ide.eclipse.adt.internal.project.BaseProjectHelper;
+
+import org.eclipse.core.resources.IFolder;
+import org.eclipse.core.resources.IProject;
+import org.eclipse.core.resources.IResource;
+import org.eclipse.core.runtime.IPath;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Helper class to generate {@link ChangedFileSet} for given projects.
+ *
+ * Also contains non project specific {@link ChangedFileSet} such as {@link #MANIFEST}
+ * and {@link #NATIVE_LIBS}
+ */
+class ChangedFileSetHelper {
+
+ final static ChangedFileSet MANIFEST;
+ final static ChangedFileSet NATIVE_LIBS;
+
+ static {
+ MANIFEST = new ChangedFileSet("manifest", //$NON-NLS-1$
+ SdkConstants.FN_ANDROID_MANIFEST_XML);
+
+ // FIXME: move compiled native libs to bin/libs/
+ NATIVE_LIBS = new ChangedFileSet(
+ "nativeLibs",
+ SdkConstants.FD_NATIVE_LIBS + "/*/*.so", //$NON-NLS-1$
+ SdkConstants.FD_NATIVE_LIBS + "/*/" + SdkConstants.FN_GDBSERVER); //$NON-NLS-1$
+ }
+
+ /**
+ * Returns a ChangedFileSet for Java resources inside a given project's source folders.
+ * @param project the project.
+ * @return a ChangedFileSet
+ */
+ static ChangedFileSet getJavaResCfs(@NonNull IProject project) {
+
+ // get the source folder for the given project.
+ IPath projectPath = project.getFullPath();
+
+ // get the source folders.
+ List<IPath> srcPaths = BaseProjectHelper.getSourceClasspaths(project);
+ List<String> paths = new ArrayList<String>(srcPaths.size());
+
+ // create a pattern for each of them.
+ for (IPath path : srcPaths) {
+ paths.add(path.makeRelativeTo(projectPath).toString() + "/**"); //$NON-NLS-1$
+ }
+
+ // custom ChangedFileSet to ignore .java files.
+ return new JavaResChangedSet("javaRes", //$NON-NLS-1$
+ paths.toArray(new String[paths.size()]));
+ }
+
+ /**
+ * Returns a {@link ChangedFileSet} for all the resources (included assets), and the output
+ * file (compiled resources
+ * @param project the project
+ * @return a ChangeFileSet
+ */
+ static ChangedFileSet getResCfs(@NonNull IProject project) {
+ // generated res is inside the project's android output folder
+ String path = getRelativeAndroidOut(project);
+
+ ChangedFileSet set = new ChangedFileSet(
+ "resources", //$NON-NLS-1$
+ SdkConstants.FD_RES + "/**", //$NON-NLS-1$
+ SdkConstants.FD_ASSETS + "/**", //$NON-NLS-1$
+ path + '/' + AdtConstants.WS_BIN_RELATIVE_BC + "/**"); //$NON-NLS-1$
+
+ // output file is based on the project's android output folder
+ set.setOutput(path + '/' + AdtConstants.FN_RESOURCES_AP_);
+
+ return set;
+ }
+
+ /**
+ * Returns a {@link ChangedFileSet} for all the resources (included assets), and the output
+ * file (compiled resources
+ * @param project the project
+ * @return a ChangeFileSet
+ */
+ static ChangedFileSet getMergedManifestCfs(@NonNull IProject project) {
+ // input path is inside the project's android output folder
+ String path = getRelativeAndroidOut(project);
+
+ ChangedFileSet set = new ChangedFileSet(
+ "mergedManifest", //$NON-NLS-1$
+ path + '/' + SdkConstants.FN_ANDROID_MANIFEST_XML);
+
+ return set;
+ }
+
+ /**
+ * Returns a {@link ChangedFileSet} for the generated R.txt file
+ * @param project the project
+ * @return a ChangeFileSet
+ */
+ static ChangedFileSet getTextSymbols(@NonNull IProject project) {
+ // input path is inside the project's android output folder
+ String path = getRelativeAndroidOut(project);
+
+ ChangedFileSet set = new ChangedFileSet(
+ "textSymbols", //$NON-NLS-1$
+ path + '/' + SdkConstants.FN_RESOURCE_TEXT);
+
+ return set;
+ }
+
+ /**
+ * Returns a {@link ChangedFileSet} for a project's javac output.
+ * @param project the project
+ * @return a ChangedFileSet
+ */
+ static ChangedFileSet getByteCodeCfs(@NonNull IProject project) {
+ // input pattern is based on the project's Java compiler's output folder
+ String path = getRelativeJavaCOut(project);
+
+ ChangedFileSet set = new ChangedFileSet("compiledCode", //$NON-NLS-1$
+ path + "/**/*" + SdkConstants.DOT_CLASS); //$NON-NLS-1$
+
+ return set;
+ }
+
+ /**
+ * Returns a {@link ChangedFileSet} for a project's complete resources, including
+ * generated resources and crunch cache.
+ * @param project the project
+ * @return a ChangeFileSet
+ */
+ static ChangedFileSet getFullResCfs(@NonNull IProject project) {
+ // generated res are in the project's android output folder
+ String path = getRelativeAndroidOut(project);
+
+ ChangedFileSet set = new ChangedFileSet("libResources", //$NON-NLS-1$
+ SdkConstants.FD_RES + "/**", //$NON-NLS-1$
+ path + '/' + SdkConstants.FD_RES + "/**"); //$NON-NLS-1$
+
+ return set;
+ }
+
+ /**
+ * Returns a {@link ChangedFileSet} for a project's whole code, including
+ * compiled bytecode, 3rd party libs, and the output file containing the Dalvik
+ * bytecode file.
+ * @param project the project
+ * @return a ChangeFileSet
+ */
+ static ChangedFileSet getCodeCfs(@NonNull IProject project) {
+ // input pattern is based on the project's Java compiler's output folder
+ String path = getRelativeJavaCOut(project);
+
+ ChangedFileSet set = new ChangedFileSet("classAndJars", //$NON-NLS-1$
+ path + "/**/*" + SdkConstants.DOT_CLASS, //$NON-NLS-1$
+ SdkConstants.FD_NATIVE_LIBS + "/*" + SdkConstants.DOT_JAR); //$NON-NLS-1$
+
+ // output file is based on the project's android output folder
+ path = getRelativeAndroidOut(project);
+ set.setOutput(path + '/' + SdkConstants.FN_APK_CLASSES_DEX);
+
+ return set;
+ }
+
+ private static String getRelativePath(@NonNull IProject project, @NonNull IResource resource) {
+ return resource.getFullPath().makeRelativeTo(project.getFullPath()).toString();
+ }
+
+ private static String getRelativeAndroidOut(@NonNull IProject project) {
+ IFolder folder = BaseProjectHelper.getAndroidOutputFolder(project);
+ return getRelativePath(project, folder);
+ }
+
+ private static String getRelativeJavaCOut(@NonNull IProject project) {
+ IFolder folder = BaseProjectHelper.getJavaOutputFolder(project);
+ return getRelativePath(project, folder);
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/builders/JavaResChangedSet.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/builders/JavaResChangedSet.java
new file mode 100644
index 000000000..6b257efbf
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/builders/JavaResChangedSet.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.internal.build.builders;
+
+import com.android.annotations.NonNull;
+import com.android.sdklib.build.ApkBuilder;
+
+import org.eclipse.core.runtime.IPath;
+
+/**
+ * Custom {@link ChangedFileSet} for java resources.
+ *
+ * This builds the set of inputs to be all the source folders of the given project,
+ * and excludes files that won't be packaged.
+ * This exclusion can't be easily described as a glob-pattern so it's overriding the default
+ * behavior instead.
+ *
+ */
+class JavaResChangedSet extends ChangedFileSet {
+
+ JavaResChangedSet(String logName, String... inputs) {
+ super(logName, inputs);
+ }
+
+ @Override
+ public boolean isInput(@NonNull String path, @NonNull IPath iPath) {
+ if (!ApkBuilder.checkFileForPackaging(iPath.lastSegment(), iPath.getFileExtension())) {
+ return false;
+ }
+ return super.isInput(path, iPath);
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/builders/PatternBasedDeltaVisitor.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/builders/PatternBasedDeltaVisitor.java
new file mode 100644
index 000000000..b52ede90c
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/builders/PatternBasedDeltaVisitor.java
@@ -0,0 +1,139 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.internal.build.builders;
+
+import com.android.ide.eclipse.adt.AdtPlugin;
+import com.android.ide.eclipse.adt.internal.build.BuildHelper;
+
+import org.eclipse.core.resources.IFolder;
+import org.eclipse.core.resources.IProject;
+import org.eclipse.core.resources.IResource;
+import org.eclipse.core.resources.IResourceDelta;
+import org.eclipse.core.resources.IResourceDeltaVisitor;
+import org.eclipse.core.runtime.CoreException;
+import org.eclipse.core.runtime.IPath;
+import org.eclipse.core.runtime.IStatus;
+
+import java.util.ArrayList;
+import java.util.IdentityHashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Delta visitor checking changed files against given glob-patterns.
+ *
+ * The visitor is given {@link ChangedFileSet} objects which contains patterns to detect change
+ * in input and output files. (Output files are only tested if the delta indicate the file
+ * was removed).
+ *
+ * After the visitor has visited the whole delta, it can be queried to see which ChangedFileSet
+ * recognized a file change. (ChangedFileSet are immutable and do not record this info).
+ */
+class PatternBasedDeltaVisitor implements IResourceDeltaVisitor {
+
+ private final static boolean DEBUG_LOG = "1".equals( //$NON-NLS-1$
+ System.getenv("ANDROID_VISITOR_DEBUG")); //$NON-NLS-1$
+
+ private final IProject mMainProject;
+ private final IProject mDeltaProject;
+
+ private final List<ChangedFileSet> mSets = new ArrayList<ChangedFileSet>();
+ private final Map<ChangedFileSet, Boolean> mResults =
+ new IdentityHashMap<ChangedFileSet, Boolean>();
+
+ private final String mLogName;
+
+ PatternBasedDeltaVisitor(IProject mainProject, IProject deltaProject, String logName) {
+ mMainProject = mainProject;
+ mDeltaProject = deltaProject;
+ mLogName = logName;
+ if (DEBUG_LOG) {
+ AdtPlugin.log(IStatus.INFO, "%s (%s): Delta for %s", //$NON-NLS-1$
+ mMainProject.getName(), mLogName, mDeltaProject.getName());
+ }
+ }
+
+ void addSet(ChangedFileSet bundle) {
+ mSets.add(bundle);
+ }
+
+ boolean checkSet(ChangedFileSet bundle) {
+ Boolean r = mResults.get(bundle);
+ if (r != null) {
+ return r.booleanValue();
+ }
+
+ return false;
+ }
+
+ @Override
+ public boolean visit(IResourceDelta delta) throws CoreException {
+ IResource resource = delta.getResource();
+
+ if (resource.getType() == IResource.FOLDER) {
+ // always visit the subfolders, unless the folder is not to be included
+ return BuildHelper.checkFolderForPackaging((IFolder)resource);
+
+ } else if (resource.getType() == IResource.FILE) {
+ IPath path = resource.getFullPath().makeRelativeTo(mDeltaProject.getFullPath());
+ String pathStr = path.toString();
+
+ // FIXME: no need to loop through all the sets once they have all said they need something (return false below and above)
+ for (ChangedFileSet set : mSets) {
+ // FIXME: should ignore sets that have already returned true.
+
+ if (set.isInput(pathStr, path)) {
+ mResults.put(set, Boolean.TRUE);
+
+ if (DEBUG_LOG) {
+ String cfs_logName = set.getLogName();
+
+ if (cfs_logName != null) {
+ AdtPlugin.log(IStatus.INFO, "%s (%s:%s): %s", //$NON-NLS-1$
+ mMainProject.getName(), mLogName, cfs_logName,
+ resource.getFullPath().toString());
+ } else {
+ AdtPlugin.log(IStatus.INFO, "%s (%s): %s", //$NON-NLS-1$
+ mMainProject.getName(), mLogName,
+ resource.getFullPath().toString());
+ }
+ }
+
+ } else if (delta.getKind() == IResourceDelta.REMOVED &&
+ set.isOutput(pathStr, path)) {
+ mResults.put(set, Boolean.TRUE);
+
+ if (DEBUG_LOG) {
+ String cfs_logName = set.getLogName();
+
+ if (cfs_logName != null) {
+ AdtPlugin.log(IStatus.INFO, "%s (%s:%s): %s", //$NON-NLS-1$
+ mMainProject.getName(), mLogName, cfs_logName,
+ resource.getFullPath().toString());
+ } else {
+ AdtPlugin.log(IStatus.INFO, "%s (%s): %s", //$NON-NLS-1$
+ mMainProject.getName(), mLogName,
+ resource.getFullPath().toString());
+ }
+ }
+ }
+ }
+ }
+
+ return true;
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/builders/PostCompilerBuilder.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/builders/PostCompilerBuilder.java
new file mode 100644
index 000000000..8aacb44ef
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/builders/PostCompilerBuilder.java
@@ -0,0 +1,946 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.internal.build.builders;
+
+import com.android.SdkConstants;
+import com.android.annotations.NonNull;
+import com.android.annotations.Nullable;
+import com.android.ide.eclipse.adt.AdtConstants;
+import com.android.ide.eclipse.adt.AdtPlugin;
+import com.android.ide.eclipse.adt.AndroidPrintStream;
+import com.android.ide.eclipse.adt.internal.build.AaptExecException;
+import com.android.ide.eclipse.adt.internal.build.AaptParser;
+import com.android.ide.eclipse.adt.internal.build.AaptResultException;
+import com.android.ide.eclipse.adt.internal.build.BuildHelper;
+import com.android.ide.eclipse.adt.internal.build.BuildHelper.ResourceMarker;
+import com.android.ide.eclipse.adt.internal.build.DexException;
+import com.android.ide.eclipse.adt.internal.build.Messages;
+import com.android.ide.eclipse.adt.internal.build.NativeLibInJarException;
+import com.android.ide.eclipse.adt.internal.lint.LintDeltaProcessor;
+import com.android.ide.eclipse.adt.internal.preferences.AdtPrefs;
+import com.android.ide.eclipse.adt.internal.preferences.AdtPrefs.BuildVerbosity;
+import com.android.ide.eclipse.adt.internal.project.ApkInstallManager;
+import com.android.ide.eclipse.adt.internal.project.BaseProjectHelper;
+import com.android.ide.eclipse.adt.internal.project.LibraryClasspathContainerInitializer;
+import com.android.ide.eclipse.adt.internal.project.ProjectHelper;
+import com.android.ide.eclipse.adt.internal.resources.manager.ResourceManager;
+import com.android.ide.eclipse.adt.internal.sdk.ProjectState;
+import com.android.ide.eclipse.adt.internal.sdk.Sdk;
+import com.android.ide.eclipse.adt.io.IFileWrapper;
+import com.android.prefs.AndroidLocation.AndroidLocationException;
+import com.android.sdklib.build.ApkBuilder;
+import com.android.sdklib.build.ApkCreationException;
+import com.android.sdklib.build.DuplicateFileException;
+import com.android.sdklib.build.IArchiveBuilder;
+import com.android.sdklib.build.SealedApkException;
+import com.android.sdklib.internal.build.DebugKeyProvider.KeytoolException;
+import com.android.xml.AndroidManifest;
+
+import org.eclipse.core.resources.IContainer;
+import org.eclipse.core.resources.IFile;
+import org.eclipse.core.resources.IFolder;
+import org.eclipse.core.resources.IMarker;
+import org.eclipse.core.resources.IProject;
+import org.eclipse.core.resources.IResource;
+import org.eclipse.core.resources.IResourceDelta;
+import org.eclipse.core.runtime.CoreException;
+import org.eclipse.core.runtime.IPath;
+import org.eclipse.core.runtime.IProgressMonitor;
+import org.eclipse.core.runtime.IStatus;
+import org.eclipse.jdt.core.IJavaModelMarker;
+import org.eclipse.jdt.core.IJavaProject;
+import org.eclipse.jdt.core.JavaCore;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+import java.util.jar.Attributes;
+import java.util.jar.JarEntry;
+import java.util.jar.JarOutputStream;
+import java.util.jar.Manifest;
+import java.util.regex.Pattern;
+
+public class PostCompilerBuilder extends BaseBuilder {
+
+ /** This ID is used in plugin.xml and in each project's .project file.
+ * It cannot be changed even if the class is renamed/moved */
+ public static final String ID = "com.android.ide.eclipse.adt.ApkBuilder"; //$NON-NLS-1$
+
+ private static final String PROPERTY_CONVERT_TO_DEX = "convertToDex"; //$NON-NLS-1$
+ private static final String PROPERTY_PACKAGE_RESOURCES = "packageResources"; //$NON-NLS-1$
+ private static final String PROPERTY_BUILD_APK = "buildApk"; //$NON-NLS-1$
+
+ /** Flag to pass to PostCompiler builder that sets if it runs or not.
+ * Set this flag whenever calling build if PostCompiler is to run
+ */
+ public final static String POST_C_REQUESTED = "RunPostCompiler"; //$NON-NLS-1$
+
+ /**
+ * Dex conversion flag. This is set to true if one of the changed/added/removed
+ * file is a .class file. Upon visiting all the delta resource, if this
+ * flag is true, then we know we'll have to make the "classes.dex" file.
+ */
+ private boolean mConvertToDex = false;
+
+ /**
+ * Package resources flag. This is set to true if one of the changed/added/removed
+ * file is a resource file. Upon visiting all the delta resource, if
+ * this flag is true, then we know we'll have to repackage the resources.
+ */
+ private boolean mPackageResources = false;
+
+ /**
+ * Final package build flag.
+ */
+ private boolean mBuildFinalPackage = false;
+
+ private AndroidPrintStream mOutStream = null;
+ private AndroidPrintStream mErrStream = null;
+
+
+ private ResourceMarker mResourceMarker = new ResourceMarker() {
+ @Override
+ public void setWarning(IResource resource, String message) {
+ BaseProjectHelper.markResource(resource, AdtConstants.MARKER_PACKAGING,
+ message, IMarker.SEVERITY_WARNING);
+ }
+ };
+
+
+ public PostCompilerBuilder() {
+ super();
+ }
+
+ @Override
+ protected void clean(IProgressMonitor monitor) throws CoreException {
+ super.clean(monitor);
+
+ // Get the project.
+ IProject project = getProject();
+
+ if (DEBUG_LOG) {
+ AdtPlugin.log(IStatus.INFO, "%s CLEAN(POST)", project.getName());
+ }
+
+ // Clear the project of the generic markers
+ removeMarkersFromContainer(project, AdtConstants.MARKER_AAPT_PACKAGE);
+ removeMarkersFromContainer(project, AdtConstants.MARKER_PACKAGING);
+
+ // also remove the files in the output folder (but not the Eclipse output folder).
+ IFolder javaOutput = BaseProjectHelper.getJavaOutputFolder(project);
+ IFolder androidOutput = BaseProjectHelper.getAndroidOutputFolder(project);
+
+ if (javaOutput.equals(androidOutput) == false) {
+ // get the content
+ IResource[] members = androidOutput.members();
+ for (IResource member : members) {
+ if (member.equals(javaOutput) == false) {
+ member.delete(true /*force*/, monitor);
+ }
+ }
+ }
+ }
+
+ // build() returns a list of project from which this project depends for future compilation.
+ @Override
+ protected IProject[] build(
+ int kind,
+ @SuppressWarnings("rawtypes") Map args,
+ IProgressMonitor monitor)
+ throws CoreException {
+ // get a project object
+ IProject project = getProject();
+
+ if (DEBUG_LOG) {
+ AdtPlugin.log(IStatus.INFO, "%s BUILD(POST)", project.getName());
+ }
+
+ // Benchmarking start
+ long startBuildTime = 0;
+ if (BuildHelper.BENCHMARK_FLAG) {
+ // End JavaC Timer
+ String msg = "BENCHMARK ADT: Ending Compilation \n BENCHMARK ADT: Time Elapsed: " + //$NON-NLS-1$
+ (System.nanoTime() - BuildHelper.sStartJavaCTime)/Math.pow(10, 6) + "ms"; //$NON-NLS-1$
+ AdtPlugin.printBuildToConsole(BuildVerbosity.ALWAYS, project, msg);
+ msg = "BENCHMARK ADT: Starting PostCompilation"; //$NON-NLS-1$
+ AdtPlugin.printBuildToConsole(BuildVerbosity.ALWAYS, project, msg);
+ startBuildTime = System.nanoTime();
+ }
+
+ // list of referenced projects. This is a mix of java projects and library projects
+ // and is computed below.
+ IProject[] allRefProjects = null;
+
+ try {
+ // get the project info
+ ProjectState projectState = Sdk.getProjectState(project);
+
+ // this can happen if the project has no project.properties.
+ if (projectState == null) {
+ return null;
+ }
+
+ boolean isLibrary = projectState.isLibrary();
+
+ // get the libraries
+ List<IProject> libProjects = projectState.getFullLibraryProjects();
+
+ IJavaProject javaProject = JavaCore.create(project);
+
+ // get the list of referenced projects.
+ List<IProject> javaProjects = ProjectHelper.getReferencedProjects(project);
+ List<IJavaProject> referencedJavaProjects = BuildHelper.getJavaProjects(
+ javaProjects);
+
+ // mix the java project and the library projects
+ final int size = libProjects.size() + javaProjects.size();
+ ArrayList<IProject> refList = new ArrayList<IProject>(size);
+ refList.addAll(libProjects);
+ refList.addAll(javaProjects);
+ allRefProjects = refList.toArray(new IProject[size]);
+
+ // get the android output folder
+ IFolder androidOutputFolder = BaseProjectHelper.getAndroidOutputFolder(project);
+ IFolder resOutputFolder = androidOutputFolder.getFolder(SdkConstants.FD_RES);
+
+ // First thing we do is go through the resource delta to not
+ // lose it if we have to abort the build for any reason.
+ if (args.containsKey(POST_C_REQUESTED)
+ && AdtPrefs.getPrefs().getBuildSkipPostCompileOnFileSave()) {
+ // Skip over flag setting
+ } else if (kind == FULL_BUILD) {
+ AdtPlugin.printBuildToConsole(BuildVerbosity.VERBOSE, project,
+ Messages.Start_Full_Apk_Build);
+
+ if (DEBUG_LOG) {
+ AdtPlugin.log(IStatus.INFO, "%s full build!", project.getName());
+ }
+
+ // Full build: we do all the steps.
+ mPackageResources = true;
+ mConvertToDex = true;
+ mBuildFinalPackage = true;
+ } else {
+ AdtPlugin.printBuildToConsole(BuildVerbosity.VERBOSE, project,
+ Messages.Start_Inc_Apk_Build);
+
+ // go through the resources and see if something changed.
+ IResourceDelta delta = getDelta(project);
+ if (delta == null) {
+ // no delta? Same as full build: we do all the steps.
+ mPackageResources = true;
+ mConvertToDex = true;
+ mBuildFinalPackage = true;
+ } else {
+
+ if (ResourceManager.isAutoBuilding() && AdtPrefs.getPrefs().isLintOnSave()) {
+ // Check for errors on save/build, if enabled
+ LintDeltaProcessor.create().process(delta);
+ }
+
+ PatternBasedDeltaVisitor dv = new PatternBasedDeltaVisitor(
+ project, project,
+ "POST:Main");
+
+ ChangedFileSet manifestCfs = ChangedFileSetHelper.getMergedManifestCfs(project);
+ dv.addSet(manifestCfs);
+
+ ChangedFileSet resCfs = ChangedFileSetHelper.getResCfs(project);
+ dv.addSet(resCfs);
+
+ ChangedFileSet androidCodeCfs = ChangedFileSetHelper.getCodeCfs(project);
+ dv.addSet(androidCodeCfs);
+
+ ChangedFileSet javaResCfs = ChangedFileSetHelper.getJavaResCfs(project);
+ dv.addSet(javaResCfs);
+ dv.addSet(ChangedFileSetHelper.NATIVE_LIBS);
+
+ delta.accept(dv);
+
+ // save the state
+ mPackageResources |= dv.checkSet(manifestCfs) || dv.checkSet(resCfs);
+
+ mConvertToDex |= dv.checkSet(androidCodeCfs);
+
+ mBuildFinalPackage |= dv.checkSet(javaResCfs) ||
+ dv.checkSet(ChangedFileSetHelper.NATIVE_LIBS);
+ }
+
+ // check the libraries
+ if (libProjects.size() > 0) {
+ for (IProject libProject : libProjects) {
+ delta = getDelta(libProject);
+ if (delta != null) {
+ PatternBasedDeltaVisitor visitor = new PatternBasedDeltaVisitor(
+ project, libProject,
+ "POST:Lib");
+
+ ChangedFileSet libResCfs = ChangedFileSetHelper.getFullResCfs(
+ libProject);
+ visitor.addSet(libResCfs);
+ visitor.addSet(ChangedFileSetHelper.NATIVE_LIBS);
+ // FIXME: add check on the library.jar?
+
+ delta.accept(visitor);
+
+ mPackageResources |= visitor.checkSet(libResCfs);
+ mBuildFinalPackage |= visitor.checkSet(
+ ChangedFileSetHelper.NATIVE_LIBS);
+ }
+ }
+ }
+
+ // also go through the delta for all the referenced projects
+ final int referencedCount = referencedJavaProjects.size();
+ for (int i = 0 ; i < referencedCount; i++) {
+ IJavaProject referencedJavaProject = referencedJavaProjects.get(i);
+ delta = getDelta(referencedJavaProject.getProject());
+ if (delta != null) {
+ IProject referencedProject = referencedJavaProject.getProject();
+ PatternBasedDeltaVisitor visitor = new PatternBasedDeltaVisitor(
+ project, referencedProject,
+ "POST:RefedProject");
+
+ ChangedFileSet javaResCfs = ChangedFileSetHelper.getJavaResCfs(referencedProject);
+ visitor.addSet(javaResCfs);
+
+ ChangedFileSet bytecodeCfs = ChangedFileSetHelper.getByteCodeCfs(referencedProject);
+ visitor.addSet(bytecodeCfs);
+
+ delta.accept(visitor);
+
+ // save the state
+ mConvertToDex |= visitor.checkSet(bytecodeCfs);
+ mBuildFinalPackage |= visitor.checkSet(javaResCfs);
+ }
+ }
+ }
+
+ // store the build status in the persistent storage
+ saveProjectBooleanProperty(PROPERTY_CONVERT_TO_DEX, mConvertToDex);
+ saveProjectBooleanProperty(PROPERTY_PACKAGE_RESOURCES, mPackageResources);
+ saveProjectBooleanProperty(PROPERTY_BUILD_APK, mBuildFinalPackage);
+
+ // Top level check to make sure the build can move forward. Only do this after recording
+ // delta changes.
+ abortOnBadSetup(javaProject, projectState);
+
+ // Get the output stream. Since the builder is created for the life of the
+ // project, they can be kept around.
+ if (mOutStream == null) {
+ mOutStream = new AndroidPrintStream(project, null /*prefix*/,
+ AdtPlugin.getOutStream());
+ mErrStream = new AndroidPrintStream(project, null /*prefix*/,
+ AdtPlugin.getOutStream());
+ }
+
+ // remove older packaging markers.
+ removeMarkersFromContainer(javaProject.getProject(), AdtConstants.MARKER_PACKAGING);
+
+ // finished with the common init and tests. Special case of the library.
+ if (isLibrary) {
+ // check the jar output file is present, if not create it.
+ IFile jarIFile = androidOutputFolder.getFile(
+ project.getName().toLowerCase() + SdkConstants.DOT_JAR);
+ if (mConvertToDex == false && jarIFile.exists() == false) {
+ mConvertToDex = true;
+ }
+
+ // also update the crunch cache always since aapt does it smartly only
+ // on the files that need it.
+ if (DEBUG_LOG) {
+ AdtPlugin.log(IStatus.INFO, "%s running crunch!", project.getName());
+ }
+ BuildHelper helper = new BuildHelper(
+ projectState,
+ mBuildToolInfo,
+ mOutStream, mErrStream,
+ false /*jumbo mode doesn't matter here*/,
+ false /*dex merger doesn't matter here*/,
+ true /*debugMode*/,
+ AdtPrefs.getPrefs().getBuildVerbosity() == BuildVerbosity.VERBOSE,
+ mResourceMarker);
+ updateCrunchCache(project, helper);
+
+ // refresh recursively bin/res folder
+ resOutputFolder.refreshLocal(IResource.DEPTH_INFINITE, monitor);
+
+ if (mConvertToDex) { // in this case this means some class files changed and
+ // we need to update the jar file.
+ if (DEBUG_LOG) {
+ AdtPlugin.log(IStatus.INFO, "%s updating jar!", project.getName());
+ }
+
+ // resource to the AndroidManifest.xml file
+ IFile manifestFile = project.getFile(SdkConstants.FN_ANDROID_MANIFEST_XML);
+ String appPackage = AndroidManifest.getPackage(new IFileWrapper(manifestFile));
+
+ IFolder javaOutputFolder = BaseProjectHelper.getJavaOutputFolder(project);
+
+ writeLibraryPackage(jarIFile, project, appPackage, javaOutputFolder);
+ saveProjectBooleanProperty(PROPERTY_CONVERT_TO_DEX, mConvertToDex = false);
+
+ // refresh the bin folder content with no recursion to update the library
+ // jar file.
+ androidOutputFolder.refreshLocal(IResource.DEPTH_ONE, monitor);
+
+ // Also update the projects. The only way to force recompile them is to
+ // reset the library container.
+ List<ProjectState> parentProjects = projectState.getParentProjects();
+ LibraryClasspathContainerInitializer.updateProject(parentProjects);
+ }
+
+ return allRefProjects;
+ }
+
+ // Check to see if we're going to launch or export. If not, we can skip
+ // the packaging and dexing process.
+ if (!args.containsKey(POST_C_REQUESTED)
+ && AdtPrefs.getPrefs().getBuildSkipPostCompileOnFileSave()) {
+ AdtPlugin.printBuildToConsole(BuildVerbosity.VERBOSE, project,
+ Messages.Skip_Post_Compiler);
+ return allRefProjects;
+ } else {
+ AdtPlugin.printBuildToConsole(BuildVerbosity.VERBOSE, project,
+ Messages.Start_Full_Post_Compiler);
+ }
+
+ // first thing we do is check that the SDK directory has been setup.
+ String osSdkFolder = AdtPlugin.getOsSdkFolder();
+
+ if (osSdkFolder.length() == 0) {
+ // this has already been checked in the precompiler. Therefore,
+ // while we do have to cancel the build, we don't have to return
+ // any error or throw anything.
+ return allRefProjects;
+ }
+
+ // do some extra check, in case the output files are not present. This
+ // will force to recreate them.
+ IResource tmp = null;
+
+ if (mPackageResources == false) {
+ // check the full resource package
+ tmp = androidOutputFolder.findMember(AdtConstants.FN_RESOURCES_AP_);
+ if (tmp == null || tmp.exists() == false) {
+ mPackageResources = true;
+ }
+ }
+
+ // check classes.dex is present. If not we force to recreate it.
+ if (mConvertToDex == false) {
+ tmp = androidOutputFolder.findMember(SdkConstants.FN_APK_CLASSES_DEX);
+ if (tmp == null || tmp.exists() == false) {
+ mConvertToDex = true;
+ }
+ }
+
+ // also check the final file(s)!
+ String finalPackageName = ProjectHelper.getApkFilename(project, null /*config*/);
+ if (mBuildFinalPackage == false) {
+ tmp = androidOutputFolder.findMember(finalPackageName);
+ if (tmp == null || (tmp instanceof IFile &&
+ tmp.exists() == false)) {
+ String msg = String.format(Messages.s_Missing_Repackaging, finalPackageName);
+ AdtPlugin.printBuildToConsole(BuildVerbosity.VERBOSE, project, msg);
+ mBuildFinalPackage = true;
+ }
+ }
+
+ // at this point we know if we need to recreate the temporary apk
+ // or the dex file, but we don't know if we simply need to recreate them
+ // because they are missing
+
+ // refresh the output directory first
+ IContainer ic = androidOutputFolder.getParent();
+ if (ic != null) {
+ ic.refreshLocal(IResource.DEPTH_ONE, monitor);
+ }
+
+ // we need to test all three, as we may need to make the final package
+ // but not the intermediary ones.
+ if (mPackageResources || mConvertToDex || mBuildFinalPackage) {
+ String forceJumboStr = projectState.getProperty(
+ AdtConstants.DEX_OPTIONS_FORCEJUMBO);
+ Boolean jumbo = Boolean.valueOf(forceJumboStr);
+
+ String dexMergerStr = projectState.getProperty(
+ AdtConstants.DEX_OPTIONS_DISABLE_MERGER);
+ Boolean dexMerger = Boolean.valueOf(dexMergerStr);
+
+ BuildHelper helper = new BuildHelper(
+ projectState,
+ mBuildToolInfo,
+ mOutStream, mErrStream,
+ jumbo.booleanValue(),
+ dexMerger.booleanValue(),
+ true /*debugMode*/,
+ AdtPrefs.getPrefs().getBuildVerbosity() == BuildVerbosity.VERBOSE,
+ mResourceMarker);
+
+ IPath androidBinLocation = androidOutputFolder.getLocation();
+ if (androidBinLocation == null) {
+ markProject(AdtConstants.MARKER_PACKAGING, Messages.Output_Missing,
+ IMarker.SEVERITY_ERROR);
+ return allRefProjects;
+ }
+ String osAndroidBinPath = androidBinLocation.toOSString();
+
+ // resource to the AndroidManifest.xml file
+ IFile manifestFile = androidOutputFolder.getFile(
+ SdkConstants.FN_ANDROID_MANIFEST_XML);
+
+ if (manifestFile == null || manifestFile.exists() == false) {
+ // mark project and exit
+ String msg = String.format(Messages.s_File_Missing,
+ SdkConstants.FN_ANDROID_MANIFEST_XML);
+ markProject(AdtConstants.MARKER_PACKAGING, msg, IMarker.SEVERITY_ERROR);
+ return allRefProjects;
+ }
+
+ // Remove the old .apk.
+ // This make sure that if the apk is corrupted, then dx (which would attempt
+ // to open it), will not fail.
+ String osFinalPackagePath = osAndroidBinPath + File.separator + finalPackageName;
+ File finalPackage = new File(osFinalPackagePath);
+
+ // if delete failed, this is not really a problem, as the final package generation
+ // handle already present .apk, and if that one failed as well, the user will be
+ // notified.
+ finalPackage.delete();
+
+ // Check if we need to package the resources.
+ if (mPackageResources) {
+ // also update the crunch cache always since aapt does it smartly only
+ // on the files that need it.
+ if (DEBUG_LOG) {
+ AdtPlugin.log(IStatus.INFO, "%s running crunch!", project.getName());
+ }
+ if (updateCrunchCache(project, helper) == false) {
+ return allRefProjects;
+ }
+
+ // refresh recursively bin/res folder
+ resOutputFolder.refreshLocal(IResource.DEPTH_INFINITE, monitor);
+
+ if (DEBUG_LOG) {
+ AdtPlugin.log(IStatus.INFO, "%s packaging resources!", project.getName());
+ }
+ // remove some aapt_package only markers.
+ removeMarkersFromContainer(project, AdtConstants.MARKER_AAPT_PACKAGE);
+
+ try {
+ helper.packageResources(manifestFile, libProjects, null /*resfilter*/,
+ 0 /*versionCode */, osAndroidBinPath,
+ AdtConstants.FN_RESOURCES_AP_);
+ } catch (AaptExecException e) {
+ BaseProjectHelper.markResource(project, AdtConstants.MARKER_PACKAGING,
+ e.getMessage(), IMarker.SEVERITY_ERROR);
+ return allRefProjects;
+ } catch (AaptResultException e) {
+ // attempt to parse the error output
+ String[] aaptOutput = e.getOutput();
+ boolean parsingError = AaptParser.parseOutput(aaptOutput, project);
+
+ // if we couldn't parse the output we display it in the console.
+ if (parsingError) {
+ AdtPlugin.printErrorToConsole(project, (Object[]) aaptOutput);
+
+ // if the exec failed, and we couldn't parse the error output (and
+ // therefore not all files that should have been marked, were marked),
+ // we put a generic marker on the project and abort.
+ BaseProjectHelper.markResource(project,
+ AdtConstants.MARKER_PACKAGING,
+ Messages.Unparsed_AAPT_Errors,
+ IMarker.SEVERITY_ERROR);
+ }
+ }
+
+ // build has been done. reset the state of the builder
+ mPackageResources = false;
+
+ // and store it
+ saveProjectBooleanProperty(PROPERTY_PACKAGE_RESOURCES, mPackageResources);
+ }
+
+ String classesDexPath = osAndroidBinPath + File.separator +
+ SdkConstants.FN_APK_CLASSES_DEX;
+
+ // then we check if we need to package the .class into classes.dex
+ if (mConvertToDex) {
+ if (DEBUG_LOG) {
+ AdtPlugin.log(IStatus.INFO, "%s running dex!", project.getName());
+ }
+ try {
+ Collection<String> dxInputPaths = helper.getCompiledCodePaths();
+
+ helper.executeDx(javaProject, dxInputPaths, classesDexPath);
+ } catch (DexException e) {
+ String message = e.getMessage();
+
+ AdtPlugin.printErrorToConsole(project, message);
+ BaseProjectHelper.markResource(project, AdtConstants.MARKER_PACKAGING,
+ message, IMarker.SEVERITY_ERROR);
+
+ Throwable cause = e.getCause();
+
+ if (cause instanceof NoClassDefFoundError
+ || cause instanceof NoSuchMethodError) {
+ AdtPlugin.printErrorToConsole(project, Messages.Incompatible_VM_Warning,
+ Messages.Requires_1_5_Error);
+ }
+
+ // dx failed, we return
+ return allRefProjects;
+ }
+
+ // build has been done. reset the state of the builder
+ mConvertToDex = false;
+
+ // and store it
+ saveProjectBooleanProperty(PROPERTY_CONVERT_TO_DEX, mConvertToDex);
+ }
+
+ // now we need to make the final package from the intermediary apk
+ // and classes.dex.
+ // This is the default package with all the resources.
+
+ try {
+ if (DEBUG_LOG) {
+ AdtPlugin.log(IStatus.INFO, "%s making final package!", project.getName());
+ }
+ helper.finalDebugPackage(
+ osAndroidBinPath + File.separator + AdtConstants.FN_RESOURCES_AP_,
+ classesDexPath, osFinalPackagePath, libProjects, mResourceMarker);
+ } catch (KeytoolException e) {
+ String eMessage = e.getMessage();
+
+ // mark the project with the standard message
+ String msg = String.format(Messages.Final_Archive_Error_s, eMessage);
+ BaseProjectHelper.markResource(project, AdtConstants.MARKER_PACKAGING, msg,
+ IMarker.SEVERITY_ERROR);
+
+ // output more info in the console
+ AdtPlugin.printErrorToConsole(project,
+ msg,
+ String.format(Messages.ApkBuilder_JAVA_HOME_is_s, e.getJavaHome()),
+ Messages.ApkBuilder_Update_or_Execute_manually_s,
+ e.getCommandLine());
+
+ AdtPlugin.log(e, msg);
+
+ return allRefProjects;
+ } catch (ApkCreationException e) {
+ String eMessage = e.getMessage();
+
+ // mark the project with the standard message
+ String msg = String.format(Messages.Final_Archive_Error_s, eMessage);
+ BaseProjectHelper.markResource(project, AdtConstants.MARKER_PACKAGING, msg,
+ IMarker.SEVERITY_ERROR);
+
+ AdtPlugin.log(e, msg);
+ } catch (AndroidLocationException e) {
+ String eMessage = e.getMessage();
+
+ // mark the project with the standard message
+ String msg = String.format(Messages.Final_Archive_Error_s, eMessage);
+ BaseProjectHelper.markResource(project, AdtConstants.MARKER_PACKAGING, msg,
+ IMarker.SEVERITY_ERROR);
+ AdtPlugin.log(e, msg);
+ } catch (NativeLibInJarException e) {
+ String msg = e.getMessage();
+
+ BaseProjectHelper.markResource(project, AdtConstants.MARKER_PACKAGING,
+ msg, IMarker.SEVERITY_ERROR);
+
+ AdtPlugin.printErrorToConsole(project, (Object[]) e.getAdditionalInfo());
+ } catch (CoreException e) {
+ // mark project and return
+ String msg = String.format(Messages.Final_Archive_Error_s, e.getMessage());
+ AdtPlugin.printErrorToConsole(project, msg);
+ BaseProjectHelper.markResource(project, AdtConstants.MARKER_PACKAGING, msg,
+ IMarker.SEVERITY_ERROR);
+ AdtPlugin.log(e, msg);
+ } catch (DuplicateFileException e) {
+ String msg1 = String.format(
+ "Found duplicate file for APK: %1$s\nOrigin 1: %2$s\nOrigin 2: %3$s",
+ e.getArchivePath(), e.getFile1(), e.getFile2());
+ String msg2 = String.format(Messages.Final_Archive_Error_s, msg1);
+ AdtPlugin.printErrorToConsole(project, msg2);
+ BaseProjectHelper.markResource(project, AdtConstants.MARKER_PACKAGING, msg2,
+ IMarker.SEVERITY_ERROR);
+ }
+
+ // we are done.
+
+ // refresh the bin folder content with no recursion.
+ androidOutputFolder.refreshLocal(IResource.DEPTH_ONE, monitor);
+
+ // build has been done. reset the state of the builder
+ mBuildFinalPackage = false;
+
+ // and store it
+ saveProjectBooleanProperty(PROPERTY_BUILD_APK, mBuildFinalPackage);
+
+ // reset the installation manager to force new installs of this project
+ ApkInstallManager.getInstance().resetInstallationFor(project);
+
+ AdtPlugin.printBuildToConsole(BuildVerbosity.VERBOSE, getProject(),
+ "Build Success!");
+ }
+ } catch (AbortBuildException e) {
+ return allRefProjects;
+ } catch (Exception exception) {
+ // try to catch other exception to actually display an error. This will be useful
+ // if we get an NPE or something so that we can at least notify the user that something
+ // went wrong.
+
+ // first check if this is a CoreException we threw to cancel the build.
+ if (exception instanceof CoreException) {
+ if (((CoreException)exception).getStatus().getSeverity() == IStatus.CANCEL) {
+ // Project is already marked with an error. Nothing to do
+ return allRefProjects;
+ }
+ }
+
+ String msg = exception.getMessage();
+ if (msg == null) {
+ msg = exception.getClass().getCanonicalName();
+ }
+
+ msg = String.format("Unknown error: %1$s", msg);
+ AdtPlugin.logAndPrintError(exception, project.getName(), msg);
+ markProject(AdtConstants.MARKER_PACKAGING, msg, IMarker.SEVERITY_ERROR);
+ }
+
+ // Benchmarking end
+ if (BuildHelper.BENCHMARK_FLAG) {
+ String msg = "BENCHMARK ADT: Ending PostCompilation. \n BENCHMARK ADT: Time Elapsed: " + //$NON-NLS-1$
+ ((System.nanoTime() - startBuildTime)/Math.pow(10, 6)) + "ms"; //$NON-NLS-1$
+ AdtPlugin.printBuildToConsole(BuildVerbosity.ALWAYS, project, msg);
+ // End Overall Timer
+ msg = "BENCHMARK ADT: Done with everything! \n BENCHMARK ADT: Time Elapsed: " + //$NON-NLS-1$
+ (System.nanoTime() - BuildHelper.sStartOverallTime)/Math.pow(10, 6) + "ms"; //$NON-NLS-1$
+ AdtPlugin.printBuildToConsole(BuildVerbosity.ALWAYS, project, msg);
+ }
+
+ return allRefProjects;
+ }
+
+ private static class JarBuilder implements IArchiveBuilder {
+
+ private static Pattern R_PATTERN = Pattern.compile("R(\\$.*)?\\.class"); //$NON-NLS-1$
+ private static String BUILD_CONFIG_CLASS = "BuildConfig.class"; //$NON-NLS-1$
+
+ private final byte[] buffer = new byte[1024];
+ private final JarOutputStream mOutputStream;
+ private final String mAppPackage;
+
+ JarBuilder(JarOutputStream outputStream, String appPackage) {
+ mOutputStream = outputStream;
+ mAppPackage = appPackage.replace('.', '/');
+ }
+
+ public void addFile(IFile file, IFolder rootFolder) throws ApkCreationException {
+ // we only package class file from the output folder
+ if (SdkConstants.EXT_CLASS.equals(file.getFileExtension()) == false) {
+ return;
+ }
+
+ IPath packageApp = file.getParent().getFullPath().makeRelativeTo(
+ rootFolder.getFullPath());
+
+ String name = file.getName();
+ // Ignore the library's R/Manifest/BuildConfig classes.
+ if (mAppPackage.equals(packageApp.toString()) &&
+ (BUILD_CONFIG_CLASS.equals(name) ||
+ R_PATTERN.matcher(name).matches())) {
+ return;
+ }
+
+ IPath path = file.getFullPath().makeRelativeTo(rootFolder.getFullPath());
+ try {
+ addFile(file.getContents(), file.getLocalTimeStamp(), path.toString());
+ } catch (ApkCreationException e) {
+ throw e;
+ } catch (Exception e) {
+ throw new ApkCreationException(e, "Failed to add %s", file);
+ }
+ }
+
+ @Override
+ public void addFile(File file, String archivePath) throws ApkCreationException,
+ SealedApkException, DuplicateFileException {
+ try {
+ FileInputStream inputStream = new FileInputStream(file);
+ long lastModified = file.lastModified();
+ addFile(inputStream, lastModified, archivePath);
+ } catch (ApkCreationException e) {
+ throw e;
+ } catch (Exception e) {
+ throw new ApkCreationException(e, "Failed to add %s", file);
+ }
+ }
+
+ private void addFile(InputStream content, long lastModified, String archivePath)
+ throws IOException, ApkCreationException {
+ // create the jar entry
+ JarEntry entry = new JarEntry(archivePath);
+ entry.setTime(lastModified);
+
+ try {
+ // add the entry to the jar archive
+ mOutputStream.putNextEntry(entry);
+
+ // read the content of the entry from the input stream, and write
+ // it into the archive.
+ int count;
+ while ((count = content.read(buffer)) != -1) {
+ mOutputStream.write(buffer, 0, count);
+ }
+ } finally {
+ try {
+ if (content != null) {
+ content.close();
+ }
+ } catch (Exception e) {
+ throw new ApkCreationException(e, "Failed to close stream");
+ }
+ }
+ }
+ }
+
+ /**
+ * Updates the crunch cache if needed and return true if the build must continue.
+ */
+ private boolean updateCrunchCache(IProject project, BuildHelper helper) {
+ try {
+ helper.updateCrunchCache();
+ } catch (AaptExecException e) {
+ BaseProjectHelper.markResource(project, AdtConstants.MARKER_PACKAGING,
+ e.getMessage(), IMarker.SEVERITY_ERROR);
+ return false;
+ } catch (AaptResultException e) {
+ // attempt to parse the error output
+ String[] aaptOutput = e.getOutput();
+ boolean parsingError = AaptParser.parseOutput(aaptOutput, project);
+ // if we couldn't parse the output we display it in the console.
+ if (parsingError) {
+ AdtPlugin.printErrorToConsole(project, (Object[]) aaptOutput);
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * Writes the library jar file.
+ * @param jarIFile the destination file
+ * @param project the library project
+ * @param appPackage the library android package
+ * @param javaOutputFolder the JDT output folder.
+ */
+ private void writeLibraryPackage(IFile jarIFile, IProject project, String appPackage,
+ IFolder javaOutputFolder) {
+
+ JarOutputStream jos = null;
+ try {
+ Manifest manifest = new Manifest();
+ Attributes mainAttributes = manifest.getMainAttributes();
+ mainAttributes.put(Attributes.Name.CLASS_PATH, "Android ADT"); //$NON-NLS-1$
+ mainAttributes.putValue("Created-By", "1.0 (Android)"); //$NON-NLS-1$ //$NON-NLS-2$
+ jos = new JarOutputStream(
+ new FileOutputStream(jarIFile.getLocation().toFile()), manifest);
+
+ JarBuilder jarBuilder = new JarBuilder(jos, appPackage);
+
+ // write the class files
+ writeClassFilesIntoJar(jarBuilder, javaOutputFolder, javaOutputFolder);
+
+ // now write the standard Java resources from the output folder
+ ApkBuilder.addSourceFolder(jarBuilder, javaOutputFolder.getLocation().toFile());
+
+ saveProjectBooleanProperty(PROPERTY_CONVERT_TO_DEX, mConvertToDex);
+ } catch (Exception e) {
+ AdtPlugin.log(e, "Failed to write jar file %s", jarIFile.getLocation().toOSString());
+ } finally {
+ if (jos != null) {
+ try {
+ jos.close();
+ } catch (IOException e) {
+ // pass
+ }
+ }
+ }
+ }
+
+ private void writeClassFilesIntoJar(JarBuilder builder, IFolder folder, IFolder rootFolder)
+ throws CoreException, IOException, ApkCreationException {
+ IResource[] members = folder.members();
+ for (IResource member : members) {
+ if (member.getType() == IResource.FOLDER) {
+ writeClassFilesIntoJar(builder, (IFolder) member, rootFolder);
+ } else if (member.getType() == IResource.FILE) {
+ IFile file = (IFile) member;
+ builder.addFile(file, rootFolder);
+ }
+ }
+ }
+
+ @Override
+ protected void startupOnInitialize() {
+ super.startupOnInitialize();
+
+ // load the build status. We pass true as the default value to
+ // force a recompile in case the property was not found
+ mConvertToDex = loadProjectBooleanProperty(PROPERTY_CONVERT_TO_DEX, true);
+ mPackageResources = loadProjectBooleanProperty(PROPERTY_PACKAGE_RESOURCES, true);
+ mBuildFinalPackage = loadProjectBooleanProperty(PROPERTY_BUILD_APK, true);
+ }
+
+ @Override
+ protected void abortOnBadSetup(
+ @NonNull IJavaProject javaProject,
+ @Nullable ProjectState projectState) throws AbortBuildException, CoreException {
+ super.abortOnBadSetup(javaProject, projectState);
+
+ IProject iProject = getProject();
+
+ // do a (hopefully quick) search for Precompiler type markers. Those are always only
+ // errors.
+ stopOnMarker(iProject, AdtConstants.MARKER_AAPT_COMPILE, IResource.DEPTH_INFINITE,
+ false /*checkSeverity*/);
+ stopOnMarker(iProject, AdtConstants.MARKER_AIDL, IResource.DEPTH_INFINITE,
+ false /*checkSeverity*/);
+ stopOnMarker(iProject, AdtConstants.MARKER_RENDERSCRIPT, IResource.DEPTH_INFINITE,
+ false /*checkSeverity*/);
+ stopOnMarker(iProject, AdtConstants.MARKER_ANDROID, IResource.DEPTH_ZERO,
+ false /*checkSeverity*/);
+
+ // do a search for JDT markers. Those can be errors or warnings
+ stopOnMarker(iProject, IJavaModelMarker.JAVA_MODEL_PROBLEM_MARKER,
+ IResource.DEPTH_INFINITE, true /*checkSeverity*/);
+ stopOnMarker(iProject, IJavaModelMarker.BUILDPATH_PROBLEM_MARKER,
+ IResource.DEPTH_INFINITE, true /*checkSeverity*/);
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/builders/PreCompilerBuilder.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/builders/PreCompilerBuilder.java
new file mode 100644
index 000000000..0d9ee4897
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/builders/PreCompilerBuilder.java
@@ -0,0 +1,1401 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.internal.build.builders;
+
+import com.android.SdkConstants;
+import com.android.annotations.NonNull;
+import com.android.annotations.Nullable;
+import com.android.ide.common.xml.ManifestData;
+import com.android.ide.eclipse.adt.AdtConstants;
+import com.android.ide.eclipse.adt.AdtPlugin;
+import com.android.ide.eclipse.adt.internal.build.AaptParser;
+import com.android.ide.eclipse.adt.internal.build.AidlProcessor;
+import com.android.ide.eclipse.adt.internal.build.Messages;
+import com.android.ide.eclipse.adt.internal.build.RenderScriptLauncher;
+import com.android.ide.eclipse.adt.internal.build.RsSourceChangeHandler;
+import com.android.ide.eclipse.adt.internal.build.SourceProcessor;
+import com.android.ide.eclipse.adt.internal.build.builders.BaseBuilder.AbortBuildException;
+import com.android.ide.eclipse.adt.internal.lint.EclipseLintClient;
+import com.android.ide.eclipse.adt.internal.preferences.AdtPrefs;
+import com.android.ide.eclipse.adt.internal.preferences.AdtPrefs.BuildVerbosity;
+import com.android.ide.eclipse.adt.internal.project.AndroidManifestHelper;
+import com.android.ide.eclipse.adt.internal.project.BaseProjectHelper;
+import com.android.ide.eclipse.adt.internal.project.FixLaunchConfig;
+import com.android.ide.eclipse.adt.internal.project.ProjectHelper;
+import com.android.ide.eclipse.adt.internal.project.XmlErrorHandler.BasicXmlErrorListener;
+import com.android.ide.eclipse.adt.internal.resources.manager.IdeScanningContext;
+import com.android.ide.eclipse.adt.internal.resources.manager.ProjectResources;
+import com.android.ide.eclipse.adt.internal.resources.manager.ResourceManager;
+import com.android.ide.eclipse.adt.internal.sdk.AdtManifestMergeCallback;
+import com.android.ide.eclipse.adt.internal.sdk.ProjectState;
+import com.android.ide.eclipse.adt.internal.sdk.Sdk;
+import com.android.ide.eclipse.adt.io.IFileWrapper;
+import com.android.ide.eclipse.adt.io.IFolderWrapper;
+import com.android.io.StreamException;
+import com.android.manifmerger.ManifestMerger;
+import com.android.manifmerger.MergerLog;
+import com.android.sdklib.AndroidVersion;
+import com.android.sdklib.BuildToolInfo;
+import com.android.sdklib.IAndroidTarget;
+import com.android.sdklib.build.RenderScriptChecker;
+import com.android.sdklib.build.RenderScriptProcessor;
+import com.android.sdklib.internal.build.BuildConfigGenerator;
+import com.android.sdklib.internal.build.SymbolLoader;
+import com.android.sdklib.internal.build.SymbolWriter;
+import com.android.sdklib.internal.project.ProjectProperties;
+import com.android.sdklib.io.FileOp;
+import com.android.sdklib.repository.FullRevision;
+import com.android.utils.ILogger;
+import com.android.utils.Pair;
+import com.android.xml.AndroidManifest;
+import com.google.common.collect.ArrayListMultimap;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Multimap;
+
+import org.eclipse.core.resources.IFile;
+import org.eclipse.core.resources.IFolder;
+import org.eclipse.core.resources.IMarker;
+import org.eclipse.core.resources.IProject;
+import org.eclipse.core.resources.IResource;
+import org.eclipse.core.resources.IResourceDelta;
+import org.eclipse.core.resources.IWorkspaceRoot;
+import org.eclipse.core.resources.ResourcesPlugin;
+import org.eclipse.core.runtime.CoreException;
+import org.eclipse.core.runtime.IPath;
+import org.eclipse.core.runtime.IProgressMonitor;
+import org.eclipse.core.runtime.IStatus;
+import org.eclipse.core.runtime.NullProgressMonitor;
+import org.eclipse.core.runtime.Path;
+import org.eclipse.jdt.core.IJavaProject;
+import org.eclipse.jdt.core.JavaCore;
+import org.xml.sax.SAXException;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+
+import javax.xml.parsers.ParserConfigurationException;
+
+/**
+ * Pre Java Compiler.
+ * This incremental builder performs 2 tasks:
+ * <ul>
+ * <li>compiles the resources located in the res/ folder, along with the
+ * AndroidManifest.xml file into the R.java class.</li>
+ * <li>compiles any .aidl files into a corresponding java file.</li>
+ * </ul>
+ *
+ */
+public class PreCompilerBuilder extends BaseBuilder {
+
+ /** This ID is used in plugin.xml and in each project's .project file.
+ * It cannot be changed even if the class is renamed/moved */
+ public static final String ID = "com.android.ide.eclipse.adt.PreCompilerBuilder"; //$NON-NLS-1$
+
+ /** Flag to pass to PreCompiler builder that the build is a release build.
+ */
+ public final static String RELEASE_REQUESTED = "android.releaseBuild"; //$NON-NLS-1$
+
+ private static final String PROPERTY_PACKAGE = "manifestPackage"; //$NON-NLS-1$
+ private static final String PROPERTY_MERGE_MANIFEST = "mergeManifest"; //$NON-NLS-1$
+ private static final String PROPERTY_COMPILE_RESOURCES = "compileResources"; //$NON-NLS-1$
+ private static final String PROPERTY_COMPILE_BUILDCONFIG = "createBuildConfig"; //$NON-NLS-1$
+ private static final String PROPERTY_BUILDCONFIG_MODE = "buildConfigMode"; //$NON-NLS-1$
+
+ private static final boolean MANIFEST_MERGER_ENABLED_DEFAULT = false;
+ private static final String MANIFEST_MERGER_PROPERTY = "manifestmerger.enabled"; //$NON-NLS-1$
+
+ /** Merge Manifest Flag. Computed from resource delta, reset after action is taken.
+ * Stored persistently in the project. */
+ private boolean mMustMergeManifest = false;
+ /** Resource compilation Flag. Computed from resource delta, reset after action is taken.
+ * Stored persistently in the project. */
+ private boolean mMustCompileResources = false;
+ /** BuildConfig Flag. Computed from resource delta, reset after action is taken.
+ * Stored persistently in the project. */
+ private boolean mMustCreateBuildConfig = false;
+ /** BuildConfig last more Flag. Computed from resource delta, reset after action is taken.
+ * Stored persistently in the project. */
+ private boolean mLastBuildConfigMode;
+
+ /** cache of the java package defined in the manifest */
+ private String mManifestPackage;
+
+ /** Output folder for generated Java File. Created on the Builder init
+ * @see #startupOnInitialize()
+ */
+ private IFolder mGenFolder;
+
+ /**
+ * Progress monitor used at the end of every build to refresh the content of the 'gen' folder
+ * and set the generated files as derived.
+ */
+ private DerivedProgressMonitor mDerivedProgressMonitor;
+
+ private AidlProcessor mAidlProcessor;
+ private RsSourceChangeHandler mRenderScriptSourceChangeHandler;
+
+ /**
+ * Progress monitor waiting the end of the process to set a persistent value
+ * in a file. This is typically used in conjunction with <code>IResource.refresh()</code>,
+ * since this call is asynchronous, and we need to wait for it to finish for the file
+ * to be known by eclipse, before we can call <code>resource.setPersistentProperty</code> on
+ * a new file.
+ */
+ private static class DerivedProgressMonitor implements IProgressMonitor {
+ private boolean mCancelled = false;
+ private boolean mDone = false;
+ private final IFolder mGenFolder;
+
+ public DerivedProgressMonitor(IFolder genFolder) {
+ mGenFolder = genFolder;
+ }
+
+ void reset() {
+ mDone = false;
+ }
+
+ @Override
+ public void beginTask(String name, int totalWork) {
+ }
+
+ @Override
+ public void done() {
+ if (mDone == false) {
+ mDone = true;
+ processChildrenOf(mGenFolder);
+ }
+ }
+
+ private void processChildrenOf(IFolder folder) {
+ IResource[] list;
+ try {
+ list = folder.members();
+ } catch (CoreException e) {
+ return;
+ }
+
+ for (IResource member : list) {
+ if (member.exists()) {
+ if (member.getType() == IResource.FOLDER) {
+ processChildrenOf((IFolder) member);
+ }
+
+ try {
+ member.setDerived(true, new NullProgressMonitor());
+ } catch (CoreException e) {
+ // This really shouldn't happen since we check that the resource
+ // exist.
+ // Worst case scenario, the resource isn't marked as derived.
+ }
+ }
+ }
+ }
+
+ @Override
+ public void internalWorked(double work) {
+ }
+
+ @Override
+ public boolean isCanceled() {
+ return mCancelled;
+ }
+
+ @Override
+ public void setCanceled(boolean value) {
+ mCancelled = value;
+ }
+
+ @Override
+ public void setTaskName(String name) {
+ }
+
+ @Override
+ public void subTask(String name) {
+ }
+
+ @Override
+ public void worked(int work) {
+ }
+ }
+
+ public PreCompilerBuilder() {
+ super();
+ }
+
+ // build() returns a list of project from which this project depends for future compilation.
+ @Override
+ protected IProject[] build(
+ int kind,
+ @SuppressWarnings("rawtypes") Map args,
+ IProgressMonitor monitor)
+ throws CoreException {
+ // get a project object
+ IProject project = getProject();
+
+ if (DEBUG_LOG) {
+ AdtPlugin.log(IStatus.INFO, "%s BUILD(PRE)", project.getName());
+ }
+
+ // For the PreCompiler, only the library projects are considered Referenced projects,
+ // as only those projects have an impact on what is generated by this builder.
+ IProject[] result = null;
+
+ IFolder resOutFolder = null;
+
+ try {
+ assert mDerivedProgressMonitor != null;
+
+ mDerivedProgressMonitor.reset();
+
+ // get the project info
+ ProjectState projectState = Sdk.getProjectState(project);
+
+ // this can happen if the project has no project.properties.
+ if (projectState == null) {
+ return null;
+ }
+
+ boolean isLibrary = projectState.isLibrary();
+
+ IAndroidTarget projectTarget = projectState.getTarget();
+
+ // get the libraries
+ List<IProject> libProjects = projectState.getFullLibraryProjects();
+ result = libProjects.toArray(new IProject[libProjects.size()]);
+
+ IJavaProject javaProject = JavaCore.create(project);
+
+ // Top level check to make sure the build can move forward.
+ abortOnBadSetup(javaProject, projectState);
+
+ // now we need to get the classpath list
+ List<IPath> sourceFolderPathList = BaseProjectHelper.getSourceClasspaths(javaProject);
+
+ IFolder androidOutputFolder = BaseProjectHelper.getAndroidOutputFolder(project);
+
+ resOutFolder = getResOutFolder(androidOutputFolder);
+
+ setupSourceProcessors(javaProject, projectState, sourceFolderPathList,
+ androidOutputFolder);
+
+ PreCompilerDeltaVisitor dv = null;
+ String javaPackage = null;
+ String minSdkVersion = null;
+
+ if (kind == FULL_BUILD) {
+ AdtPlugin.printBuildToConsole(BuildVerbosity.VERBOSE, project,
+ Messages.Start_Full_Pre_Compiler);
+
+ if (DEBUG_LOG) {
+ AdtPlugin.log(IStatus.INFO, "%s full build!", project.getName());
+ }
+
+ // do some clean up.
+ doClean(project, monitor);
+
+ mMustMergeManifest = true;
+ mMustCompileResources = true;
+ mMustCreateBuildConfig = true;
+
+ mAidlProcessor.prepareFullBuild(project);
+ mRenderScriptSourceChangeHandler.prepareFullBuild();
+ } else {
+ AdtPlugin.printBuildToConsole(BuildVerbosity.VERBOSE, project,
+ Messages.Start_Inc_Pre_Compiler);
+
+ // Go through the resources and see if something changed.
+ // Even if the mCompileResources flag is true from a previously aborted
+ // build, we need to go through the Resource delta to get a possible
+ // list of aidl files to compile/remove.
+ IResourceDelta delta = getDelta(project);
+ if (delta == null) {
+ mMustCompileResources = true;
+
+ mAidlProcessor.prepareFullBuild(project);
+ mRenderScriptSourceChangeHandler.prepareFullBuild();
+ } else {
+ dv = new PreCompilerDeltaVisitor(this, sourceFolderPathList,
+ mAidlProcessor.getChangeHandler(),
+ mRenderScriptSourceChangeHandler);
+ delta.accept(dv);
+
+ // Check to see if Manifest.xml, Manifest.java, or R.java have changed:
+ mMustCompileResources |= dv.getCompileResources();
+ mMustMergeManifest |= dv.hasManifestChanged();
+
+ // Notify the ResourceManager:
+ ResourceManager resManager = ResourceManager.getInstance();
+
+ if (ResourceManager.isAutoBuilding()) {
+ ProjectResources projectResources = resManager.getProjectResources(project);
+
+ IdeScanningContext context = new IdeScanningContext(projectResources,
+ project, true);
+
+ boolean wasCleared = projectResources.ensureInitialized();
+
+ if (!wasCleared) {
+ resManager.processDelta(delta, context);
+ }
+
+ // Check whether this project or its dependencies (libraries) have
+ // resources that need compilation
+ if (wasCleared || context.needsFullAapt()) {
+ mMustCompileResources = true;
+
+ // Must also call markAaptRequested on the project to not just
+ // store "aapt required" on this project, but also on any projects
+ // depending on this project if it's a library project
+ ResourceManager.markAaptRequested(project);
+ }
+
+ // Update error markers in the source editor
+ if (!mMustCompileResources) {
+ context.updateMarkers(false /* async */);
+ }
+ } // else: already processed the deltas in ResourceManager's IRawDeltaListener
+
+ mAidlProcessor.doneVisiting(project);
+
+ // get the java package from the visitor
+ javaPackage = dv.getManifestPackage();
+ minSdkVersion = dv.getMinSdkVersion();
+ }
+ }
+
+ // Has anyone marked this project as needing aapt? Typically done when
+ // one of the library projects this project depends on has changed
+ mMustCompileResources |= ResourceManager.isAaptRequested(project);
+
+ // if the main manifest didn't change, then we check for the library
+ // ones (will trigger manifest merging too)
+ if (libProjects.size() > 0) {
+ for (IProject libProject : libProjects) {
+ IResourceDelta delta = getDelta(libProject);
+ if (delta != null) {
+ PatternBasedDeltaVisitor visitor = new PatternBasedDeltaVisitor(
+ project, libProject,
+ "PRE:LibManifest"); //$NON-NLS-1$
+ visitor.addSet(ChangedFileSetHelper.MANIFEST);
+
+ ChangedFileSet textSymbolCFS = null;
+ if (isLibrary == false) {
+ textSymbolCFS = ChangedFileSetHelper.getTextSymbols(
+ libProject);
+ visitor.addSet(textSymbolCFS);
+ }
+
+ delta.accept(visitor);
+
+ mMustMergeManifest |= visitor.checkSet(ChangedFileSetHelper.MANIFEST);
+
+ if (textSymbolCFS != null) {
+ mMustCompileResources |= visitor.checkSet(textSymbolCFS);
+ }
+
+ // no need to test others if we have all flags at true.
+ if (mMustMergeManifest &&
+ (mMustCompileResources || textSymbolCFS == null)) {
+ break;
+ }
+ }
+ }
+ }
+
+ // store the build status in the persistent storage
+ saveProjectBooleanProperty(PROPERTY_MERGE_MANIFEST, mMustMergeManifest);
+ saveProjectBooleanProperty(PROPERTY_COMPILE_RESOURCES, mMustCompileResources);
+ saveProjectBooleanProperty(PROPERTY_COMPILE_BUILDCONFIG, mMustCreateBuildConfig);
+
+ // if there was some XML errors, we just return w/o doing
+ // anything since we've put some markers in the files anyway.
+ if (dv != null && dv.mXmlError) {
+ AdtPlugin.printErrorToConsole(project, Messages.Xml_Error);
+
+ return result;
+ }
+
+ if (projectState.getRenderScriptSupportMode()) {
+ FullRevision minBuildToolsRev = new FullRevision(19,0,3);
+ if (mBuildToolInfo.getRevision().compareTo(minBuildToolsRev) == -1) {
+ String msg = "RenderScript support mode requires Build-Tools 19.0.3 or later.";
+ AdtPlugin.printErrorToConsole(project, msg);
+ markProject(AdtConstants.MARKER_ADT, msg, IMarker.SEVERITY_ERROR);
+
+ return result;
+ }
+ }
+
+ // get the manifest file
+ IFile manifestFile = ProjectHelper.getManifest(project);
+
+ if (manifestFile == null) {
+ String msg = String.format(Messages.s_File_Missing,
+ SdkConstants.FN_ANDROID_MANIFEST_XML);
+ AdtPlugin.printErrorToConsole(project, msg);
+ markProject(AdtConstants.MARKER_ADT, msg, IMarker.SEVERITY_ERROR);
+
+ return result;
+
+ // TODO: document whether code below that uses manifest (which is now guaranteed
+ // to be null) will actually be executed or not.
+ }
+
+ // lets check the XML of the manifest first, if that hasn't been done by the
+ // resource delta visitor yet.
+ if (dv == null || dv.getCheckedManifestXml() == false) {
+ BasicXmlErrorListener errorListener = new BasicXmlErrorListener();
+ try {
+ ManifestData parser = AndroidManifestHelper.parseUnchecked(
+ new IFileWrapper(manifestFile),
+ true /*gather data*/,
+ errorListener);
+
+ if (errorListener.mHasXmlError == true) {
+ // There was an error in the manifest, its file has been marked
+ // by the XmlErrorHandler. The stopBuild() call below will abort
+ // this with an exception.
+ String msg = String.format(Messages.s_Contains_Xml_Error,
+ SdkConstants.FN_ANDROID_MANIFEST_XML);
+ AdtPlugin.printBuildToConsole(BuildVerbosity.VERBOSE, project, msg);
+ markProject(AdtConstants.MARKER_ADT, msg, IMarker.SEVERITY_ERROR);
+
+ return result;
+ }
+
+ // Get the java package from the parser.
+ // This can be null if the parsing failed because the resource is out of sync,
+ // in which case the error will already have been logged anyway.
+ if (parser != null) {
+ javaPackage = parser.getPackage();
+ minSdkVersion = parser.getMinSdkVersionString();
+ }
+ } catch (StreamException e) {
+ handleStreamException(e);
+
+ return result;
+ } catch (ParserConfigurationException e) {
+ String msg = String.format(
+ "Bad parser configuration for %s: %s",
+ manifestFile.getFullPath(),
+ e.getMessage());
+
+ handleException(e, msg);
+ return result;
+
+ } catch (SAXException e) {
+ String msg = String.format(
+ "Parser exception for %s: %s",
+ manifestFile.getFullPath(),
+ e.getMessage());
+
+ handleException(e, msg);
+ return result;
+ } catch (IOException e) {
+ String msg = String.format(
+ "I/O error for %s: %s",
+ manifestFile.getFullPath(),
+ e.getMessage());
+
+ handleException(e, msg);
+ return result;
+ }
+ }
+
+ int minSdkValue = -1;
+
+ if (minSdkVersion != null) {
+ try {
+ minSdkValue = Integer.parseInt(minSdkVersion);
+ } catch (NumberFormatException e) {
+ // it's ok, it means minSdkVersion contains a (hopefully) valid codename.
+ }
+
+ AndroidVersion targetVersion = projectTarget.getVersion();
+
+ // remove earlier marker from the manifest
+ removeMarkersFromResource(manifestFile, AdtConstants.MARKER_ADT);
+
+ if (minSdkValue != -1) {
+ String codename = targetVersion.getCodename();
+ if (codename != null) {
+ // integer minSdk when the target is a preview => fatal error
+ String msg = String.format(
+ "Platform %1$s is a preview and requires application manifest to set %2$s to '%1$s'",
+ codename, AndroidManifest.ATTRIBUTE_MIN_SDK_VERSION);
+ AdtPlugin.printErrorToConsole(project, msg);
+ BaseProjectHelper.markResource(manifestFile, AdtConstants.MARKER_ADT,
+ msg, IMarker.SEVERITY_ERROR);
+ return result;
+ } else if (minSdkValue > targetVersion.getApiLevel()) {
+ // integer minSdk is too high for the target => warning
+ String msg = String.format(
+ "Attribute %1$s (%2$d) is higher than the project target API level (%3$d)",
+ AndroidManifest.ATTRIBUTE_MIN_SDK_VERSION,
+ minSdkValue, targetVersion.getApiLevel());
+ AdtPlugin.printBuildToConsole(BuildVerbosity.VERBOSE, project, msg);
+ BaseProjectHelper.markResource(manifestFile, AdtConstants.MARKER_ADT,
+ msg, IMarker.SEVERITY_WARNING);
+ }
+ } else {
+ // looks like the min sdk is a codename, check it matches the codename
+ // of the platform
+ String codename = targetVersion.getCodename();
+ if (codename == null) {
+ // platform is not a preview => fatal error
+ String msg = String.format(
+ "Manifest attribute '%1$s' is set to '%2$s'. Integer is expected.",
+ AndroidManifest.ATTRIBUTE_MIN_SDK_VERSION, minSdkVersion);
+ AdtPlugin.printErrorToConsole(project, msg);
+ BaseProjectHelper.markResource(manifestFile, AdtConstants.MARKER_ADT,
+ msg, IMarker.SEVERITY_ERROR);
+ return result;
+ } else if (codename.equals(minSdkVersion) == false) {
+ // platform and manifest codenames don't match => fatal error.
+ String msg = String.format(
+ "Value of manifest attribute '%1$s' does not match platform codename '%2$s'",
+ AndroidManifest.ATTRIBUTE_MIN_SDK_VERSION, codename);
+ AdtPlugin.printErrorToConsole(project, msg);
+ BaseProjectHelper.markResource(manifestFile, AdtConstants.MARKER_ADT,
+ msg, IMarker.SEVERITY_ERROR);
+ return result;
+ }
+
+ // if we get there, the minSdkVersion is a codename matching the target
+ // platform codename. In this case we set minSdkValue to the previous API
+ // level, as it's used by source processors.
+ minSdkValue = targetVersion.getApiLevel();
+ }
+ } else if (projectTarget.getVersion().isPreview()) {
+ // else the minSdkVersion is not set but we are using a preview target.
+ // Display an error
+ String codename = projectTarget.getVersion().getCodename();
+ String msg = String.format(
+ "Platform %1$s is a preview and requires application manifests to set %2$s to '%1$s'",
+ codename, AndroidManifest.ATTRIBUTE_MIN_SDK_VERSION);
+ AdtPlugin.printErrorToConsole(project, msg);
+ BaseProjectHelper.markResource(manifestFile, AdtConstants.MARKER_ADT, msg,
+ IMarker.SEVERITY_ERROR);
+ return result;
+ }
+
+ if (javaPackage == null || javaPackage.length() == 0) {
+ // looks like the AndroidManifest file isn't valid.
+ String msg = String.format(Messages.s_Doesnt_Declare_Package_Error,
+ SdkConstants.FN_ANDROID_MANIFEST_XML);
+ AdtPlugin.printErrorToConsole(project, msg);
+ BaseProjectHelper.markResource(manifestFile, AdtConstants.MARKER_ADT,
+ msg, IMarker.SEVERITY_ERROR);
+
+ return result;
+ } else if (javaPackage.indexOf('.') == -1) {
+ // The application package name does not contain 2+ segments!
+ String msg = String.format(
+ "Application package '%1$s' must have a minimum of 2 segments.",
+ SdkConstants.FN_ANDROID_MANIFEST_XML);
+ AdtPlugin.printErrorToConsole(project, msg);
+ BaseProjectHelper.markResource(manifestFile, AdtConstants.MARKER_ADT,
+ msg, IMarker.SEVERITY_ERROR);
+
+ return result;
+ }
+
+ // at this point we have the java package. We need to make sure it's not a different
+ // package than the previous one that were built.
+ if (javaPackage.equals(mManifestPackage) == false) {
+ // The manifest package has changed, the user may want to update
+ // the launch configuration
+ if (mManifestPackage != null) {
+ AdtPlugin.printBuildToConsole(BuildVerbosity.VERBOSE, project,
+ Messages.Checking_Package_Change);
+
+ FixLaunchConfig flc = new FixLaunchConfig(project, mManifestPackage,
+ javaPackage);
+ flc.start();
+ }
+
+ // record the new manifest package, and save it.
+ mManifestPackage = javaPackage;
+ saveProjectStringProperty(PROPERTY_PACKAGE, mManifestPackage);
+
+ // force a clean
+ doClean(project, monitor);
+ mMustMergeManifest = true;
+ mMustCompileResources = true;
+ mMustCreateBuildConfig = true;
+ mAidlProcessor.prepareFullBuild(project);
+ mRenderScriptSourceChangeHandler.prepareFullBuild();
+
+ saveProjectBooleanProperty(PROPERTY_MERGE_MANIFEST, mMustMergeManifest);
+ saveProjectBooleanProperty(PROPERTY_COMPILE_RESOURCES, mMustCompileResources);
+ saveProjectBooleanProperty(PROPERTY_COMPILE_BUILDCONFIG, mMustCreateBuildConfig);
+ }
+
+ try {
+ handleBuildConfig(args);
+ } catch (IOException e) {
+ handleException(e, "Failed to create BuildConfig class");
+ return result;
+ }
+
+ // merge the manifest
+ if (mMustMergeManifest) {
+ boolean enabled = MANIFEST_MERGER_ENABLED_DEFAULT;
+ String propValue = projectState.getProperty(MANIFEST_MERGER_PROPERTY);
+ if (propValue != null) {
+ enabled = Boolean.valueOf(propValue);
+ }
+
+ if (mergeManifest(androidOutputFolder, libProjects, enabled) == false) {
+ return result;
+ }
+ }
+
+ List<File> libProjectsOut = new ArrayList<File>(libProjects.size());
+ for (IProject libProject : libProjects) {
+ libProjectsOut.add(
+ BaseProjectHelper.getAndroidOutputFolder(libProject)
+ .getLocation().toFile());
+ }
+
+ // run the source processors
+ int processorStatus = SourceProcessor.COMPILE_STATUS_NONE;
+
+
+ try {
+ processorStatus |= mAidlProcessor.compileFiles(this,
+ project, projectTarget, sourceFolderPathList,
+ libProjectsOut, monitor);
+ } catch (Throwable t) {
+ handleException(t, "Failed to run aidl. Check workspace log for detail.");
+ return result;
+ }
+
+ try {
+ processorStatus |= compileRs(minSdkValue, projectState, androidOutputFolder,
+ resOutFolder, monitor);
+ } catch (Throwable t) {
+ handleException(t, "Failed to run renderscript. Check workspace log for detail.");
+ return result;
+ }
+
+ // if a processor created some resources file, force recompilation of the resources.
+ if ((processorStatus & SourceProcessor.COMPILE_STATUS_RES) != 0) {
+ mMustCompileResources = true;
+ // save the current state before attempting the compilation
+ saveProjectBooleanProperty(PROPERTY_COMPILE_RESOURCES, mMustCompileResources);
+ }
+
+ // handle the resources, after the processors are run since some (renderscript)
+ // generate resources.
+ boolean compiledTheResources = mMustCompileResources;
+ if (mMustCompileResources) {
+ if (DEBUG_LOG) {
+ AdtPlugin.log(IStatus.INFO, "%s compiling resources!", project.getName());
+ }
+
+ IFile proguardFile = null;
+ if (projectState.getProperty(ProjectProperties.PROPERTY_PROGUARD_CONFIG) != null) {
+ proguardFile = androidOutputFolder.getFile(AdtConstants.FN_AAPT_PROGUARD);
+ }
+
+ handleResources(project, javaPackage, projectTarget, manifestFile, resOutFolder,
+ libProjects, isLibrary, proguardFile);
+ }
+
+ if (processorStatus == SourceProcessor.COMPILE_STATUS_NONE &&
+ compiledTheResources == false) {
+ AdtPlugin.printBuildToConsole(BuildVerbosity.VERBOSE, project,
+ Messages.Nothing_To_Compile);
+ }
+ } catch (AbortBuildException e) {
+ return result;
+ } finally {
+ // refresh the 'gen' source folder. Once this is done with the custom progress
+ // monitor to mark all new files as derived
+ mGenFolder.refreshLocal(IResource.DEPTH_INFINITE, mDerivedProgressMonitor);
+ if (resOutFolder != null) {
+ resOutFolder.refreshLocal(IResource.DEPTH_INFINITE, mDerivedProgressMonitor);
+ }
+ }
+
+ return result;
+ }
+
+ private IFolder getResOutFolder(IFolder androidOutputFolder) {
+ return androidOutputFolder.getFolder(AdtConstants.WS_BIN_RELATIVE_BC);
+ }
+
+ @Override
+ protected void clean(IProgressMonitor monitor) throws CoreException {
+ super.clean(monitor);
+
+ if (DEBUG_LOG) {
+ AdtPlugin.log(IStatus.INFO, "%s CLEAN(PRE)", getProject().getName());
+ }
+
+ doClean(getProject(), monitor);
+ if (mGenFolder != null) {
+ mGenFolder.refreshLocal(IResource.DEPTH_INFINITE, monitor);
+ }
+ }
+
+ private void doClean(IProject project, IProgressMonitor monitor) throws CoreException {
+ AdtPlugin.printBuildToConsole(BuildVerbosity.VERBOSE, project,
+ Messages.Removing_Generated_Classes);
+
+ // remove all the derived resources from the 'gen' source folder.
+ if (mGenFolder != null && mGenFolder.exists()) {
+ // gen folder should not be derived, but previous version could set it to derived
+ // so we make sure this isn't the case (or it'll get deleted by the clean)
+ mGenFolder.setDerived(false, monitor);
+
+ removeDerivedResources(mGenFolder, monitor);
+ }
+
+ // Clear the project of the generic markers
+ removeMarkersFromContainer(project, AdtConstants.MARKER_AAPT_COMPILE);
+ removeMarkersFromContainer(project, AdtConstants.MARKER_XML);
+ removeMarkersFromContainer(project, AdtConstants.MARKER_AIDL);
+ removeMarkersFromContainer(project, AdtConstants.MARKER_RENDERSCRIPT);
+ removeMarkersFromContainer(project, AdtConstants.MARKER_MANIFMERGER);
+ removeMarkersFromContainer(project, AdtConstants.MARKER_ANDROID);
+
+ // Also clean up lint
+ EclipseLintClient.clearMarkers(project);
+
+ // clean the project repo
+ ProjectResources res = ResourceManager.getInstance().getProjectResources(project);
+ res.clear();
+ }
+
+ @Override
+ protected void startupOnInitialize() {
+ try {
+ super.startupOnInitialize();
+
+ IProject project = getProject();
+
+ // load the previous IFolder and java package.
+ mManifestPackage = loadProjectStringProperty(PROPERTY_PACKAGE);
+
+ // get the source folder in which all the Java files are created
+ mGenFolder = project.getFolder(SdkConstants.FD_GEN_SOURCES);
+ mDerivedProgressMonitor = new DerivedProgressMonitor(mGenFolder);
+
+ // Load the current compile flags. We ask for true if not found to force a recompile.
+ mMustMergeManifest = loadProjectBooleanProperty(PROPERTY_MERGE_MANIFEST, true);
+ mMustCompileResources = loadProjectBooleanProperty(PROPERTY_COMPILE_RESOURCES, true);
+ mMustCreateBuildConfig = loadProjectBooleanProperty(PROPERTY_COMPILE_BUILDCONFIG, true);
+ Boolean v = ProjectHelper.loadBooleanProperty(project, PROPERTY_BUILDCONFIG_MODE);
+ if (v == null) {
+ // no previous build config mode? force regenerate
+ mMustCreateBuildConfig = true;
+ } else {
+ mLastBuildConfigMode = v;
+ }
+
+ } catch (Throwable throwable) {
+ AdtPlugin.log(throwable, "Failed to finish PrecompilerBuilder#startupOnInitialize()");
+ }
+ }
+
+ private void setupSourceProcessors(@NonNull IJavaProject javaProject,
+ @NonNull ProjectState projectState,
+ @NonNull List<IPath> sourceFolderPathList,
+ @NonNull IFolder androidOutputFolder) {
+ if (mAidlProcessor == null) {
+ mAidlProcessor = new AidlProcessor(javaProject, mBuildToolInfo, mGenFolder);
+ } else {
+ mAidlProcessor.setBuildToolInfo(mBuildToolInfo);
+ }
+
+ List<File> sourceFolders = Lists.newArrayListWithCapacity(sourceFolderPathList.size());
+ IWorkspaceRoot root = ResourcesPlugin.getWorkspace().getRoot();
+
+ for (IPath path : sourceFolderPathList) {
+ IResource resource = root.findMember(path);
+ if (resource != null && resource.exists() && resource.getType() == IResource.FOLDER) {
+ IPath fullPath = resource.getLocation();
+ if (fullPath != null) {
+ sourceFolders.add(fullPath.toFile());
+ }
+ }
+ }
+
+ RenderScriptChecker checker = new RenderScriptChecker(sourceFolders,
+ androidOutputFolder.getLocation().toFile());
+ mRenderScriptSourceChangeHandler = new RsSourceChangeHandler(checker);
+ }
+
+ private int compileRs(int minSdkValue,
+ @NonNull ProjectState projectState,
+ @NonNull IFolder androidOutputFolder,
+ @NonNull IFolder resOutFolder,
+ @NonNull IProgressMonitor monitor)
+ throws IOException, InterruptedException {
+ if (!mRenderScriptSourceChangeHandler.mustCompile()) {
+ return SourceProcessor.COMPILE_STATUS_NONE;
+ }
+
+ RenderScriptChecker checker = mRenderScriptSourceChangeHandler.getChecker();
+
+ List<File> inputs = checker.findInputFiles();
+ List<File> importFolders = checker.getSourceFolders();
+ File buildFolder = androidOutputFolder.getLocation().toFile();
+
+
+ // get the renderscript target
+ int rsTarget = minSdkValue == -1 ? 11 : minSdkValue;
+ String rsTargetStr = projectState.getProperty(ProjectProperties.PROPERTY_RS_TARGET);
+ if (rsTargetStr != null) {
+ try {
+ rsTarget = Integer.parseInt(rsTargetStr);
+ } catch (NumberFormatException e) {
+ handleException(e, String.format(
+ "Property %s is not an integer.",
+ ProjectProperties.PROPERTY_RS_TARGET));
+ return SourceProcessor.COMPILE_STATUS_NONE;
+ }
+ }
+
+ RenderScriptProcessor processor = new RenderScriptProcessor(
+ inputs,
+ importFolders,
+ buildFolder,
+ mGenFolder.getLocation().toFile(),
+ resOutFolder.getLocation().toFile(),
+ new File(buildFolder, SdkConstants.FD_RS_OBJ),
+ new File(buildFolder, SdkConstants.FD_RS_LIBS),
+ mBuildToolInfo,
+ rsTarget,
+ false /*debugBuild, always false for now*/,
+ 3,
+ projectState.getRenderScriptSupportMode());
+
+ // clean old dependency files fiest
+ checker.cleanDependencies();
+
+ // then clean old output files
+ processor.cleanOldOutput(checker.getOldOutputs());
+
+ RenderScriptLauncher launcher = new RenderScriptLauncher(
+ getProject(),
+ mGenFolder,
+ resOutFolder,
+ monitor,
+ AdtPrefs.getPrefs().getBuildVerbosity() == BuildVerbosity.VERBOSE /*verbose*/);
+
+ // and run the build
+ processor.build(launcher);
+
+ return SourceProcessor.COMPILE_STATUS_CODE | SourceProcessor.COMPILE_STATUS_RES;
+ }
+
+ @SuppressWarnings("deprecation")
+ private void handleBuildConfig(@SuppressWarnings("rawtypes") Map args)
+ throws IOException, CoreException {
+ boolean debugMode = !args.containsKey(RELEASE_REQUESTED);
+
+ BuildConfigGenerator generator = new BuildConfigGenerator(
+ mGenFolder.getLocation().toOSString(), mManifestPackage, debugMode);
+
+ if (mMustCreateBuildConfig == false) {
+ // check the file is present.
+ IFolder folder = getGenManifestPackageFolder();
+ if (folder.exists(new Path(BuildConfigGenerator.BUILD_CONFIG_NAME)) == false) {
+ mMustCreateBuildConfig = true;
+ AdtPlugin.printBuildToConsole(BuildVerbosity.VERBOSE, getProject(),
+ String.format("Class %1$s is missing!",
+ BuildConfigGenerator.BUILD_CONFIG_NAME));
+ } else if (debugMode != mLastBuildConfigMode) {
+ // else if the build mode changed, force creation
+ mMustCreateBuildConfig = true;
+ AdtPlugin.printBuildToConsole(BuildVerbosity.VERBOSE, getProject(),
+ String.format("Different build mode, must update %1$s!",
+ BuildConfigGenerator.BUILD_CONFIG_NAME));
+ }
+ }
+
+ if (mMustCreateBuildConfig) {
+ if (DEBUG_LOG) {
+ AdtPlugin.log(IStatus.INFO, "%s generating BuilderConfig!", getProject().getName());
+ }
+
+ AdtPlugin.printBuildToConsole(BuildVerbosity.VERBOSE, getProject(),
+ String.format("Generating %1$s...", BuildConfigGenerator.BUILD_CONFIG_NAME));
+ generator.generate();
+
+ mMustCreateBuildConfig = false;
+ saveProjectBooleanProperty(PROPERTY_COMPILE_BUILDCONFIG, mMustCreateBuildConfig);
+ saveProjectBooleanProperty(PROPERTY_BUILDCONFIG_MODE, mLastBuildConfigMode = debugMode);
+ }
+ }
+
+ private boolean mergeManifest(IFolder androidOutFolder, List<IProject> libProjects,
+ boolean enabled) throws CoreException {
+ if (DEBUG_LOG) {
+ AdtPlugin.log(IStatus.INFO, "%s merging manifests!", getProject().getName());
+ }
+
+ IFile outFile = androidOutFolder.getFile(SdkConstants.FN_ANDROID_MANIFEST_XML);
+ IFile manifest = getProject().getFile(SdkConstants.FN_ANDROID_MANIFEST_XML);
+
+ // remove existing markers from the manifest.
+ // FIXME: only remove from manifest once the markers are put there.
+ removeMarkersFromResource(getProject(), AdtConstants.MARKER_MANIFMERGER);
+
+ // If the merging is not enabled or if there's no library then we simply copy the
+ // manifest over.
+ if (enabled == false || libProjects.size() == 0) {
+ try {
+ new FileOp().copyFile(manifest.getLocation().toFile(),
+ outFile.getLocation().toFile());
+
+ outFile.refreshLocal(IResource.DEPTH_INFINITE, mDerivedProgressMonitor);
+
+ saveProjectBooleanProperty(PROPERTY_MERGE_MANIFEST, mMustMergeManifest = false);
+ } catch (IOException e) {
+ handleException(e, "Failed to copy Manifest");
+ return false;
+ }
+ } else {
+ final ArrayList<String> errors = new ArrayList<String>();
+
+ // TODO change MergerLog.wrapSdkLog by a custom IMergerLog that will create
+ // and maintain error markers.
+ ManifestMerger merger = new ManifestMerger(
+ MergerLog.wrapSdkLog(new ILogger() {
+ @Override
+ public void warning(@NonNull String warningFormat, Object... args) {
+ AdtPlugin.printToConsole(getProject(), String.format(warningFormat, args));
+ }
+
+ @Override
+ public void info(@NonNull String msgFormat, Object... args) {
+ AdtPlugin.printToConsole(getProject(), String.format(msgFormat, args));
+ }
+
+ @Override
+ public void verbose(@NonNull String msgFormat, Object... args) {
+ info(msgFormat, args);
+ }
+
+ @Override
+ public void error(@Nullable Throwable t, @Nullable String errorFormat,
+ Object... args) {
+ errors.add(String.format(errorFormat, args));
+ }
+ }),
+ new AdtManifestMergeCallback());
+
+ File[] libManifests = new File[libProjects.size()];
+ int libIndex = 0;
+ for (IProject lib : libProjects) {
+ libManifests[libIndex++] = lib.getFile(SdkConstants.FN_ANDROID_MANIFEST_XML)
+ .getLocation().toFile();
+ }
+
+ if (merger.process(
+ outFile.getLocation().toFile(),
+ manifest.getLocation().toFile(),
+ libManifests,
+ null /*injectAttributes*/, null /*packageOverride*/) == false) {
+ if (errors.size() > 1) {
+ StringBuilder sb = new StringBuilder();
+ for (String s : errors) {
+ sb.append(s).append('\n');
+ }
+
+ markProject(AdtConstants.MARKER_MANIFMERGER, sb.toString(),
+ IMarker.SEVERITY_ERROR);
+
+ } else if (errors.size() == 1) {
+ markProject(AdtConstants.MARKER_MANIFMERGER, errors.get(0),
+ IMarker.SEVERITY_ERROR);
+ } else {
+ markProject(AdtConstants.MARKER_MANIFMERGER, "Unknown error merging manifest",
+ IMarker.SEVERITY_ERROR);
+ }
+ return false;
+ }
+
+ outFile.refreshLocal(IResource.DEPTH_INFINITE, mDerivedProgressMonitor);
+ saveProjectBooleanProperty(PROPERTY_MERGE_MANIFEST, mMustMergeManifest = false);
+ }
+
+ return true;
+ }
+
+ /**
+ * Handles resource changes and regenerate whatever files need regenerating.
+ * @param project the main project
+ * @param javaPackage the app package for the main project
+ * @param projectTarget the target of the main project
+ * @param manifest the {@link IFile} representing the project manifest
+ * @param libProjects the library dependencies
+ * @param isLibrary if the project is a library project
+ * @throws CoreException
+ * @throws AbortBuildException
+ */
+ private void handleResources(IProject project, String javaPackage, IAndroidTarget projectTarget,
+ IFile manifest, IFolder resOutFolder, List<IProject> libProjects, boolean isLibrary,
+ IFile proguardFile) throws CoreException, AbortBuildException {
+ // get the resource folder
+ IFolder resFolder = project.getFolder(AdtConstants.WS_RESOURCES);
+
+ // get the file system path
+ IPath outputLocation = mGenFolder.getLocation();
+ IPath resLocation = resFolder.getLocation();
+ IPath manifestLocation = manifest == null ? null : manifest.getLocation();
+
+ // those locations have to exist for us to do something!
+ if (outputLocation != null && resLocation != null
+ && manifestLocation != null) {
+ String osOutputPath = outputLocation.toOSString();
+ String osResPath = resLocation.toOSString();
+ String osManifestPath = manifestLocation.toOSString();
+
+ // remove the aapt markers
+ removeMarkersFromResource(manifest, AdtConstants.MARKER_AAPT_COMPILE);
+ removeMarkersFromContainer(resFolder, AdtConstants.MARKER_AAPT_COMPILE);
+
+ AdtPlugin.printBuildToConsole(BuildVerbosity.VERBOSE, project,
+ Messages.Preparing_Generated_Files);
+
+ // we need to figure out where to store the R class.
+ // get the parent folder for R.java and update mManifestPackageSourceFolder
+ IFolder mainPackageFolder = getGenManifestPackageFolder();
+
+ // handle libraries
+ ArrayList<IFolder> libResFolders = Lists.newArrayList();
+ ArrayList<Pair<File, String>> libRFiles = Lists.newArrayList();
+ if (libProjects != null) {
+ for (IProject lib : libProjects) {
+ IFolder libResFolder = lib.getFolder(SdkConstants.FD_RES);
+ if (libResFolder.exists()) {
+ libResFolders.add(libResFolder);
+ }
+
+ try {
+ // get the package of the library, and if it's different form the
+ // main project, generate the R class for it too.
+ String libJavaPackage = AndroidManifest.getPackage(new IFolderWrapper(lib));
+ if (libJavaPackage.equals(javaPackage) == false) {
+
+ IFolder libOutput = BaseProjectHelper.getAndroidOutputFolder(lib);
+ File libOutputFolder = libOutput.getLocation().toFile();
+
+ libRFiles.add(Pair.of(
+ new File(libOutputFolder, "R.txt"),
+ libJavaPackage));
+
+ }
+ } catch (Exception e) {
+ }
+ }
+ }
+
+ String proguardFilePath = proguardFile != null ?
+ proguardFile.getLocation().toOSString(): null;
+
+ File resOutFile = resOutFolder.getLocation().toFile();
+ String resOutPath = resOutFile.isDirectory() ? resOutFile.getAbsolutePath() : null;
+
+ execAapt(project, projectTarget, osOutputPath, resOutPath, osResPath, osManifestPath,
+ mainPackageFolder, libResFolders, libRFiles, isLibrary, proguardFilePath);
+ }
+ }
+
+ /**
+ * Executes AAPT to generate R.java/Manifest.java
+ * @param project the main project
+ * @param projectTarget the main project target
+ * @param osOutputPath the OS output path for the generated file. This is the source folder, not
+ * the package folder.
+ * @param osResPath the OS path to the res folder for the main project
+ * @param osManifestPath the OS path to the manifest of the main project
+ * @param packageFolder the IFolder that will contain the generated file. Unlike
+ * <var>osOutputPath</var> this is the direct parent of the generated files.
+ * If <var>customJavaPackage</var> is not null, this must match the new destination triggered
+ * by its value.
+ * @param libResFolders the list of res folders for the library.
+ * @param libRFiles a list of R files for the libraries.
+ * @param isLibrary if the project is a library project
+ * @param proguardFile an optional path to store proguard information
+ * @throws AbortBuildException
+ */
+ @SuppressWarnings("deprecation")
+ private void execAapt(IProject project, IAndroidTarget projectTarget, String osOutputPath,
+ String osBcOutPath, String osResPath, String osManifestPath, IFolder packageFolder,
+ ArrayList<IFolder> libResFolders, List<Pair<File, String>> libRFiles,
+ boolean isLibrary, String proguardFile)
+ throws AbortBuildException {
+
+ // We actually need to delete the manifest.java as it may become empty and
+ // in this case aapt doesn't generate an empty one, but instead doesn't
+ // touch it.
+ IFile manifestJavaFile = packageFolder.getFile(SdkConstants.FN_MANIFEST_CLASS);
+ manifestJavaFile.getLocation().toFile().delete();
+
+ // launch aapt: create the command line
+ ArrayList<String> array = new ArrayList<String>();
+
+ String aaptPath = mBuildToolInfo.getPath(BuildToolInfo.PathId.AAPT);
+
+ array.add(aaptPath);
+ array.add("package"); //$NON-NLS-1$
+ array.add("-m"); //$NON-NLS-1$
+ if (AdtPrefs.getPrefs().getBuildVerbosity() == BuildVerbosity.VERBOSE) {
+ array.add("-v"); //$NON-NLS-1$
+ }
+
+ if (isLibrary) {
+ array.add("--non-constant-id"); //$NON-NLS-1$
+ }
+
+ if (libResFolders.size() > 0) {
+ array.add("--auto-add-overlay"); //$NON-NLS-1$
+ }
+
+ // If a library or has libraries, generate a text version of the R symbols.
+ File outputFolder = BaseProjectHelper.getAndroidOutputFolder(project).getLocation()
+ .toFile();
+
+ if (isLibrary || !libRFiles.isEmpty()) {
+ array.add("--output-text-symbols"); //$NON-NLS-1$
+ array.add(outputFolder.getAbsolutePath());
+ }
+
+ array.add("-J"); //$NON-NLS-1$
+ array.add(osOutputPath);
+ array.add("-M"); //$NON-NLS-1$
+ array.add(osManifestPath);
+ if (osBcOutPath != null) {
+ array.add("-S"); //$NON-NLS-1$
+ array.add(osBcOutPath);
+ }
+ array.add("-S"); //$NON-NLS-1$
+ array.add(osResPath);
+ for (IFolder libResFolder : libResFolders) {
+ array.add("-S"); //$NON-NLS-1$
+ array.add(libResFolder.getLocation().toOSString());
+ }
+
+ array.add("-I"); //$NON-NLS-1$
+ array.add(projectTarget.getPath(IAndroidTarget.ANDROID_JAR));
+
+ // use the proguard file
+ if (proguardFile != null && proguardFile.length() > 0) {
+ array.add("-G");
+ array.add(proguardFile);
+ }
+
+ if (AdtPrefs.getPrefs().getBuildVerbosity() == BuildVerbosity.VERBOSE) {
+ StringBuilder sb = new StringBuilder();
+ for (String c : array) {
+ sb.append(c);
+ sb.append(' ');
+ }
+ String cmd_line = sb.toString();
+ AdtPlugin.printToConsole(project, cmd_line);
+ }
+
+ // launch
+ try {
+ // launch the command line process
+ Process process = Runtime.getRuntime().exec(
+ array.toArray(new String[array.size()]));
+
+ // list to store each line of stderr
+ ArrayList<String> stdErr = new ArrayList<String>();
+
+ // get the output and return code from the process
+ int returnCode = grabProcessOutput(process, stdErr);
+
+ // attempt to parse the error output
+ boolean parsingError = AaptParser.parseOutput(stdErr, project);
+
+ // if we couldn't parse the output we display it in the console.
+ if (parsingError) {
+ if (returnCode != 0) {
+ AdtPlugin.printErrorToConsole(project, stdErr.toArray());
+ } else {
+ AdtPlugin.printBuildToConsole(BuildVerbosity.NORMAL,
+ project, stdErr.toArray());
+ }
+ }
+
+ if (returnCode != 0) {
+ // if the exec failed, and we couldn't parse the error output
+ // (and therefore not all files that should have been marked,
+ // were marked), we put a generic marker on the project and abort.
+ if (parsingError) {
+ markProject(AdtConstants.MARKER_ADT,
+ Messages.Unparsed_AAPT_Errors, IMarker.SEVERITY_ERROR);
+ } else if (stdErr.size() == 0) {
+ // no parsing error because sdterr was empty. We still need to put
+ // a marker otherwise there's no user visible feedback.
+ markProject(AdtConstants.MARKER_ADT,
+ String.format(Messages.AAPT_Exec_Error_d, returnCode),
+ IMarker.SEVERITY_ERROR);
+ }
+
+ AdtPlugin.printBuildToConsole(BuildVerbosity.VERBOSE, project,
+ Messages.AAPT_Error);
+
+ // abort if exec failed.
+ throw new AbortBuildException();
+ }
+
+ // now if the project has libraries, R needs to be created for each libraries
+ // unless this is a library.
+ if (isLibrary == false && !libRFiles.isEmpty()) {
+ File rFile = new File(outputFolder, SdkConstants.FN_RESOURCE_TEXT);
+ // if the project has no resources, the file could not exist.
+ if (rFile.isFile()) {
+ // Load the full symbols from the full R.txt file.
+ SymbolLoader fullSymbolValues = new SymbolLoader(rFile);
+ fullSymbolValues.load();
+
+ Multimap<String, SymbolLoader> libMap = ArrayListMultimap.create();
+
+ // First pass processing the libraries, collecting them by packageName,
+ // and ignoring the ones that have the same package name as the application
+ // (since that R class was already created).
+
+ for (Pair<File, String> lib : libRFiles) {
+ String libPackage = lib.getSecond();
+ File rText = lib.getFirst();
+
+ if (rText.isFile()) {
+ // load the lib symbols
+ SymbolLoader libSymbols = new SymbolLoader(rText);
+ libSymbols.load();
+
+ // store these symbols by associating them with the package name.
+ libMap.put(libPackage, libSymbols);
+ }
+ }
+
+ // now loop on all the package names, merge all the symbols to write,
+ // and write them
+ for (String packageName : libMap.keySet()) {
+ Collection<SymbolLoader> symbols = libMap.get(packageName);
+
+ SymbolWriter writer = new SymbolWriter(osOutputPath, packageName,
+ fullSymbolValues);
+ for (SymbolLoader symbolLoader : symbols) {
+ writer.addSymbolsToWrite(symbolLoader);
+ }
+ writer.write();
+ }
+ }
+ }
+
+ } catch (IOException e1) {
+ // something happen while executing the process,
+ // mark the project and exit
+ String msg;
+ String path = array.get(0);
+ if (!new File(path).exists()) {
+ msg = String.format(Messages.AAPT_Exec_Error_s, path);
+ } else {
+ String description = e1.getLocalizedMessage();
+ if (e1.getCause() != null && e1.getCause() != e1) {
+ description = description + ": " + e1.getCause().getLocalizedMessage();
+ }
+ msg = String.format(Messages.AAPT_Exec_Error_Other_s, description);
+ }
+
+ markProject(AdtConstants.MARKER_ADT, msg, IMarker.SEVERITY_ERROR);
+
+ // Add workaround for the Linux problem described here:
+ // http://developer.android.com/sdk/installing.html#troubleshooting
+ // There are various posts on StackOverflow elsewhere where people are asking
+ // about aapt failing to run, so even though this is documented in the
+ // Troubleshooting section add an error message to help with this
+ // scenario.
+ if (SdkConstants.CURRENT_PLATFORM == SdkConstants.PLATFORM_LINUX
+ && System.getProperty("os.arch").endsWith("64") //$NON-NLS-1$ //$NON-NLS-2$
+ && new File(aaptPath).exists()
+ && new File("/usr/bin/apt-get").exists()) { //$NON-NLS-1$
+ markProject(AdtConstants.MARKER_ADT,
+ "Hint: On 64-bit systems, make sure the 32-bit libraries are installed: \"sudo apt-get install ia32-libs\" or on some systems, \"sudo apt-get install lib32z1\"",
+ IMarker.SEVERITY_ERROR);
+ // Note - this uses SEVERITY_ERROR even though it's really SEVERITY_INFO because
+ // we want this error message to show up adjacent to the aapt error message
+ // (and Eclipse sorts by priority)
+ }
+
+ // This interrupts the build.
+ throw new AbortBuildException();
+ } catch (InterruptedException e) {
+ // we got interrupted waiting for the process to end...
+ // mark the project and exit
+ String msg = String.format(Messages.AAPT_Exec_Error_s, array.get(0));
+ markProject(AdtConstants.MARKER_ADT, msg, IMarker.SEVERITY_ERROR);
+
+ // This interrupts the build.
+ throw new AbortBuildException();
+ } finally {
+ // we've at least attempted to run aapt, save the fact that we don't have to
+ // run it again, unless there's a new resource change.
+ saveProjectBooleanProperty(PROPERTY_COMPILE_RESOURCES,
+ mMustCompileResources = false);
+ ResourceManager.clearAaptRequest(project);
+ }
+ }
+
+ /**
+ * Creates a relative {@link IPath} from a java package.
+ * @param javaPackageName the java package.
+ */
+ private IPath getJavaPackagePath(String javaPackageName) {
+ // convert the java package into path
+ String[] segments = javaPackageName.split(AdtConstants.RE_DOT);
+
+ StringBuilder path = new StringBuilder();
+ for (String s : segments) {
+ path.append(AdtConstants.WS_SEP_CHAR);
+ path.append(s);
+ }
+
+ return new Path(path.toString());
+ }
+
+ /**
+ * Returns an {@link IFolder} (located inside the 'gen' source folder), that matches the
+ * package defined in the manifest. This {@link IFolder} may not actually exist
+ * (aapt will create it anyway).
+ * @return the {@link IFolder} that will contain the R class or null if
+ * the folder was not found.
+ * @throws CoreException
+ */
+ private IFolder getGenManifestPackageFolder() throws CoreException {
+ // get the path for the package
+ IPath packagePath = getJavaPackagePath(mManifestPackage);
+
+ // get a folder for this path under the 'gen' source folder, and return it.
+ // This IFolder may not reference an actual existing folder.
+ return mGenFolder.getFolder(packagePath);
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/builders/PreCompilerDeltaVisitor.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/builders/PreCompilerDeltaVisitor.java
new file mode 100644
index 000000000..57316f568
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/builders/PreCompilerDeltaVisitor.java
@@ -0,0 +1,417 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.internal.build.builders;
+
+import com.android.SdkConstants;
+import com.android.ide.common.xml.ManifestData;
+import com.android.ide.eclipse.adt.AdtConstants;
+import com.android.ide.eclipse.adt.AdtPlugin;
+import com.android.ide.eclipse.adt.internal.build.Messages;
+import com.android.ide.eclipse.adt.internal.build.SourceChangeHandler;
+import com.android.ide.eclipse.adt.internal.build.builders.BaseBuilder.BaseDeltaVisitor;
+import com.android.ide.eclipse.adt.internal.preferences.AdtPrefs.BuildVerbosity;
+import com.android.ide.eclipse.adt.internal.project.AndroidManifestHelper;
+import com.android.ide.eclipse.adt.internal.project.BaseProjectHelper;
+import com.android.ide.eclipse.adt.io.IFileWrapper;
+import com.google.common.collect.Lists;
+
+import org.eclipse.core.resources.IContainer;
+import org.eclipse.core.resources.IFile;
+import org.eclipse.core.resources.IFolder;
+import org.eclipse.core.resources.IResource;
+import org.eclipse.core.resources.IResourceDelta;
+import org.eclipse.core.resources.IResourceDeltaVisitor;
+import org.eclipse.core.resources.IWorkspaceRoot;
+import org.eclipse.core.resources.ResourcesPlugin;
+import org.eclipse.core.runtime.CoreException;
+import org.eclipse.core.runtime.IPath;
+
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * Resource Delta visitor for the pre-compiler.
+ * <p/>This delta visitor only cares about files that are the source or the result of actions of the
+ * {@link PreCompilerBuilder}:
+ * <ul><li>R.java/Manifest.java generated by compiling the resources</li>
+ * <li>Any Java files generated by <code>aidl</code></li></ul>.
+ *
+ * Therefore it looks for the following:
+ * <ul><li>Any modification in the resource folder</li>
+ * <li>Removed files from the source folder receiving generated Java files</li>
+ * <li>Any modification to aidl files.</li>
+ *
+ */
+class PreCompilerDeltaVisitor extends BaseDeltaVisitor implements IResourceDeltaVisitor {
+
+ // Result fields.
+ private boolean mChangedManifest = false;
+
+ /**
+ * Compile flag. This is set to true if one of the changed/added/removed
+ * files is Manifest.java, or R.java. All other file changes
+ * will be taken care of by ResourceManager.
+ */
+ private boolean mCompileResources = false;
+
+ /** Manifest check/parsing flag. */
+ private boolean mCheckedManifestXml = false;
+
+ /** Application Package, gathered from the parsing of the manifest */
+ private String mJavaPackage = null;
+ /** minSDKVersion attribute value, gathered from the parsing of the manifest */
+ private String mMinSdkVersion = null;
+
+ // Internal usage fields.
+ /**
+ * In Resource folder flag. This allows us to know if we're in the
+ * resource folder.
+ */
+ private boolean mInRes = false;
+
+ /**
+ * Current Source folder. This allows us to know if we're in a source
+ * folder, and which folder.
+ */
+ private IFolder mSourceFolder = null;
+
+ /** List of source folders. */
+ private final List<IPath> mSourceFolders;
+ private boolean mIsGenSourceFolder = false;
+
+ private final List<SourceChangeHandler> mSourceChangeHandlers = Lists.newArrayList();
+ private final IWorkspaceRoot mRoot;
+
+ private IFolder mAndroidOutputFolder;
+
+ public PreCompilerDeltaVisitor(BaseBuilder builder, List<IPath> sourceFolders,
+ SourceChangeHandler... handlers) {
+ super(builder);
+ mSourceFolders = sourceFolders;
+ mRoot = ResourcesPlugin.getWorkspace().getRoot();
+
+ mSourceChangeHandlers.addAll(Arrays.asList(handlers));
+
+ mAndroidOutputFolder = BaseProjectHelper.getAndroidOutputFolder(builder.getProject());
+ }
+
+ /**
+ * Get whether Manifest.java, Manifest.xml, or R.java have changed
+ * @return true if any of Manifest.xml, Manifest.java, or R.java have been modified
+ */
+ public boolean getCompileResources() {
+ return mCompileResources || mChangedManifest;
+ }
+
+ public boolean hasManifestChanged() {
+ return mChangedManifest;
+ }
+
+ /**
+ * Returns whether the manifest file was parsed/checked for error during the resource delta
+ * visiting.
+ */
+ public boolean getCheckedManifestXml() {
+ return mCheckedManifestXml;
+ }
+
+ /**
+ * Returns the manifest package if the manifest was checked/parsed.
+ * <p/>
+ * This can return null in two cases:
+ * <ul>
+ * <li>The manifest was not part of the resource change delta, and the manifest was
+ * not checked/parsed ({@link #getCheckedManifestXml()} returns <code>false</code>)</li>
+ * <li>The manifest was parsed ({@link #getCheckedManifestXml()} returns <code>true</code>),
+ * but the package declaration is missing</li>
+ * </ul>
+ * @return the manifest package or null.
+ */
+ public String getManifestPackage() {
+ return mJavaPackage;
+ }
+
+ /**
+ * Returns the minSDkVersion attribute from the manifest if it was checked/parsed.
+ * <p/>
+ * This can return null in two cases:
+ * <ul>
+ * <li>The manifest was not part of the resource change delta, and the manifest was
+ * not checked/parsed ({@link #getCheckedManifestXml()} returns <code>false</code>)</li>
+ * <li>The manifest was parsed ({@link #getCheckedManifestXml()} returns <code>true</code>),
+ * but the package declaration is missing</li>
+ * </ul>
+ * @return the minSdkVersion or null.
+ */
+ public String getMinSdkVersion() {
+ return mMinSdkVersion;
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see org.eclipse.core.resources.IResourceDeltaVisitor
+ * #visit(org.eclipse.core.resources.IResourceDelta)
+ */
+ @Override
+ public boolean visit(IResourceDelta delta) throws CoreException {
+ // we are only going to look for changes in res/, source folders and in
+ // AndroidManifest.xml since the delta visitor goes through the main
+ // folder before its children we can check when the path segment
+ // count is 2 (format will be /$Project/folder) and make sure we are
+ // processing res/, source folders or AndroidManifest.xml
+
+ IResource resource = delta.getResource();
+ IPath path = resource.getFullPath();
+ String[] segments = path.segments();
+
+ // since the delta visitor also visits the root we return true if
+ // segments.length = 1
+ if (segments.length == 1) {
+ // this is always the Android project since we call
+ // Builder#getDelta(IProject) on the project itself.
+ return true;
+ } else if (segments.length == 2) {
+ // if we are at an item directly under the root directory,
+ // then we are not yet in a source or resource folder
+ mInRes = false;
+ mSourceFolder = null;
+
+ if (SdkConstants.FD_RESOURCES.equalsIgnoreCase(segments[1])) {
+ // this is the resource folder that was modified. we want to
+ // see its content.
+
+ // since we're going to visit its children next, we set the
+ // flag
+ mInRes = true;
+ mSourceFolder = null;
+ return true;
+ } else if (SdkConstants.FN_ANDROID_MANIFEST_XML.equalsIgnoreCase(segments[1])) {
+ // any change in the manifest could trigger a new R.java
+ // class, so we don't need to check the delta kind
+ if (delta.getKind() != IResourceDelta.REMOVED) {
+ // clean the error markers on the file.
+ IFile manifestFile = (IFile)resource;
+
+ if (manifestFile.exists()) {
+ manifestFile.deleteMarkers(AdtConstants.MARKER_XML, true,
+ IResource.DEPTH_ZERO);
+ manifestFile.deleteMarkers(AdtConstants.MARKER_ANDROID, true,
+ IResource.DEPTH_ZERO);
+ }
+
+ // parse the manifest for data and error
+ ManifestData manifestData = AndroidManifestHelper.parse(
+ new IFileWrapper(manifestFile), true /*gatherData*/, this);
+
+ if (manifestData != null) {
+ mJavaPackage = manifestData.getPackage();
+ mMinSdkVersion = manifestData.getMinSdkVersionString();
+ }
+
+ mCheckedManifestXml = true;
+ }
+ mChangedManifest = true;
+
+ // we don't want to go to the children, not like they are
+ // any for this resource anyway.
+ return false;
+ }
+ }
+
+ // at this point we can either be in the source folder or in the
+ // resource folder or in a different folder that contains a source
+ // folder.
+ // This is due to not all source folder being src/. Some could be
+ // something/somethingelse/src/
+
+ // so first we test if we already know we are in a source or
+ // resource folder.
+
+ if (mSourceFolder != null) {
+ // if we are in the res folder, we are looking for the following changes:
+ // - added/removed/modified aidl files.
+ // - missing R.java file
+
+ // if the resource is a folder, we just go straight to the children
+ if (resource.getType() == IResource.FOLDER) {
+ return true;
+ }
+
+ if (resource.getType() != IResource.FILE) {
+ return false;
+ }
+ IFile file = (IFile)resource;
+
+ // get the modification kind
+ int kind = delta.getKind();
+
+ // we process normal source folder and the 'gen' source folder differently.
+ if (mIsGenSourceFolder) {
+ // this is the generated java file source folder.
+ // - if R.java/Manifest.java are removed/modified, we recompile the resources
+ // - if aidl files are removed/modified, we recompile them.
+
+ boolean outputWarning = false;
+
+ String fileName = resource.getName();
+
+ // Special case of R.java/Manifest.java.
+ if (SdkConstants.FN_RESOURCE_CLASS.equals(fileName) ||
+ SdkConstants.FN_MANIFEST_CLASS.equals(fileName)) {
+ // if it was removed, there's a possibility that it was removed due to a
+ // package change, or an aidl that was removed, but the only thing
+ // that will happen is that we'll have an extra build. Not much of a problem.
+ mCompileResources = true;
+
+ // we want a warning
+ outputWarning = true;
+ } else {
+ // look to see if this file was generated by a processor.
+ for (SourceChangeHandler handler : mSourceChangeHandlers) {
+ if (handler.handleGeneratedFile(file, kind)) {
+ outputWarning = true;
+ break; // there shouldn't be 2 processors that handle the same file.
+ }
+ }
+ }
+
+ if (outputWarning) {
+ if (kind == IResourceDelta.REMOVED) {
+ // We print an error just so that it's red, but it's just a warning really.
+ String msg = String.format(Messages.s_Removed_Recreating_s, fileName);
+ AdtPlugin.printErrorToConsole(mBuilder.getProject(), msg);
+ } else if (kind == IResourceDelta.CHANGED) {
+ // the file was modified manually! we can't allow it.
+ String msg = String.format(Messages.s_Modified_Manually_Recreating_s,
+ fileName);
+ AdtPlugin.printErrorToConsole(mBuilder.getProject(), msg);
+ }
+ }
+ } else {
+ // this is another source folder.
+ for (SourceChangeHandler handler : mSourceChangeHandlers) {
+ handler.handleSourceFile(file, kind);
+ }
+ }
+
+ // no children.
+ return false;
+ } else if (mInRes) {
+ // if we are in the res folder, we are looking for the following
+ // changes:
+ // - added/removed/modified xml files.
+ // - added/removed files of any other type
+
+ // if the resource is a folder, we just go straight to the
+ // children
+ if (resource.getType() == IResource.FOLDER) {
+ return true;
+ }
+
+ // get the extension of the resource
+ String ext = resource.getFileExtension();
+ int kind = delta.getKind();
+
+ String p = resource.getProjectRelativePath().toString();
+ String message = null;
+ switch (kind) {
+ case IResourceDelta.CHANGED:
+ // display verbose message
+ message = String.format(Messages.s_Modified_Recreating_s, p);
+ break;
+ case IResourceDelta.ADDED:
+ // display verbose message
+ message = String.format(Messages.Added_s_s_Needs_Updating, p,
+ SdkConstants.FN_RESOURCE_CLASS);
+ break;
+ case IResourceDelta.REMOVED:
+ // display verbose message
+ message = String.format(Messages.s_Removed_s_Needs_Updating, p,
+ SdkConstants.FN_RESOURCE_CLASS);
+ break;
+ }
+ if (message != null) {
+ AdtPlugin.printBuildToConsole(BuildVerbosity.VERBOSE,
+ mBuilder.getProject(), message);
+ }
+
+ // If it's an XML resource, check the syntax
+ if (SdkConstants.EXT_XML.equalsIgnoreCase(ext) && kind != IResourceDelta.REMOVED) {
+ // check xml Validity
+ mBuilder.checkXML(resource, this);
+ }
+ // Whether or not to generate R.java for a changed resource is taken care of by the
+ // Resource Manager.
+ } else if (resource instanceof IFolder) {
+ // first check if we are in the android output folder.
+ if (resource.equals(mAndroidOutputFolder)) {
+ // we want to visit the merged manifest.
+ return true;
+ }
+
+ // in this case we may be inside a folder that contains a source
+ // folder, go through the list of known source folders
+
+ for (IPath sourceFolderPath : mSourceFolders) {
+ // first check if they match exactly.
+ if (sourceFolderPath.equals(path)) {
+ // this is a source folder!
+ mInRes = false;
+ mSourceFolder = getFolder(sourceFolderPath); // all non null due to test above
+ mIsGenSourceFolder = path.segmentCount() == 2 &&
+ path.segment(1).equals(SdkConstants.FD_GEN_SOURCES);
+ return true;
+ }
+
+ // check if we are on the way to a source folder.
+ int count = sourceFolderPath.matchingFirstSegments(path);
+ if (count == path.segmentCount()) {
+ mInRes = false;
+ return true;
+ }
+ }
+
+ // if we're here, we are visiting another folder
+ // like /$Project/bin/ for instance (we get notified for changes
+ // in .class!)
+ // This could also be another source folder and we have found
+ // R.java in a previous source folder
+ // We don't want to visit its children
+ return false;
+ }
+
+ return false;
+ }
+
+ /**
+ * Returns a handle to the folder identified by the given path in this container.
+ * <p/>The different with {@link IContainer#getFolder(IPath)} is that this returns a non
+ * null object only if the resource actually exists and is a folder (and not a file)
+ * @param path the path of the folder to return.
+ * @return a handle to the folder if it exists, or null otherwise.
+ */
+ private IFolder getFolder(IPath path) {
+ IResource resource = mRoot.findMember(path);
+ if (resource != null && resource.exists() && resource.getType() == IResource.FOLDER) {
+ return (IFolder)resource;
+ }
+
+ return null;
+ }
+
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/builders/ResourceManagerBuilder.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/builders/ResourceManagerBuilder.java
new file mode 100644
index 000000000..8e01cca29
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/build/builders/ResourceManagerBuilder.java
@@ -0,0 +1,317 @@
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.internal.build.builders;
+
+import com.android.SdkConstants;
+import com.android.ide.eclipse.adt.AdtConstants;
+import com.android.ide.eclipse.adt.AdtPlugin;
+import com.android.ide.eclipse.adt.internal.build.Messages;
+import com.android.ide.eclipse.adt.internal.preferences.AdtPrefs;
+import com.android.ide.eclipse.adt.internal.preferences.AdtPrefs.BuildVerbosity;
+import com.android.ide.eclipse.adt.internal.project.BaseProjectHelper;
+import com.android.ide.eclipse.adt.internal.project.ProjectHelper;
+import com.android.ide.eclipse.adt.internal.sdk.ProjectState;
+import com.android.ide.eclipse.adt.internal.sdk.Sdk;
+import com.android.sdklib.BuildToolInfo;
+import com.android.sdklib.IAndroidTarget;
+import com.android.utils.Pair;
+
+import org.eclipse.core.resources.IFolder;
+import org.eclipse.core.resources.IMarker;
+import org.eclipse.core.resources.IProject;
+import org.eclipse.core.resources.IResource;
+import org.eclipse.core.resources.IWorkspaceRoot;
+import org.eclipse.core.resources.IncrementalProjectBuilder;
+import org.eclipse.core.resources.ResourcesPlugin;
+import org.eclipse.core.runtime.CoreException;
+import org.eclipse.core.runtime.IPath;
+import org.eclipse.core.runtime.IProgressMonitor;
+import org.eclipse.core.runtime.IStatus;
+import org.eclipse.core.runtime.Status;
+import org.eclipse.core.runtime.SubProgressMonitor;
+import org.eclipse.core.runtime.jobs.Job;
+import org.eclipse.jdt.core.IClasspathEntry;
+import org.eclipse.jdt.core.IJavaProject;
+import org.eclipse.jdt.core.JavaCore;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Resource manager builder whose only purpose is to refresh the resource folder
+ * so that the other builder use an up to date version.
+ */
+public class ResourceManagerBuilder extends BaseBuilder {
+
+ public static final String ID = "com.android.ide.eclipse.adt.ResourceManagerBuilder"; //$NON-NLS-1$
+
+ public ResourceManagerBuilder() {
+ super();
+ }
+
+ @Override
+ protected void clean(IProgressMonitor monitor) throws CoreException {
+ super.clean(monitor);
+
+ // Get the project.
+ IProject project = getProject();
+
+ // Clear the project of the generic markers
+ removeMarkersFromContainer(project, AdtConstants.MARKER_ADT);
+ }
+
+ // build() returns a list of project from which this project depends for future compilation.
+ @SuppressWarnings("unchecked")
+ @Override
+ protected IProject[] build(int kind, Map args, IProgressMonitor monitor)
+ throws CoreException {
+ // Get the project.
+ final IProject project = getProject();
+ IJavaProject javaProject = JavaCore.create(project);
+
+ // Clear the project of the generic markers
+ removeMarkersFromContainer(project, AdtConstants.MARKER_ADT);
+
+ // check for existing target marker, in which case we abort.
+ // (this means: no SDK, no target, or unresolvable target.)
+ try {
+ abortOnBadSetup(javaProject, null);
+ } catch (AbortBuildException e) {
+ return null;
+ }
+
+ // Check the compiler compliance level, displaying the error message
+ // since this is the first builder.
+ Pair<Integer, String> result = ProjectHelper.checkCompilerCompliance(project);
+ String errorMessage = null;
+ switch (result.getFirst().intValue()) {
+ case ProjectHelper.COMPILER_COMPLIANCE_LEVEL:
+ errorMessage = Messages.Requires_Compiler_Compliance_s;
+ break;
+ case ProjectHelper.COMPILER_COMPLIANCE_SOURCE:
+ errorMessage = Messages.Requires_Source_Compatibility_s;
+ break;
+ case ProjectHelper.COMPILER_COMPLIANCE_CODEGEN_TARGET:
+ errorMessage = Messages.Requires_Class_Compatibility_s;
+ break;
+ }
+
+ if (errorMessage != null) {
+ errorMessage = String.format(errorMessage,
+ result.getSecond() == null ? "(no value)" : result.getSecond());
+
+ if (JavaCore.VERSION_1_7.equals(result.getSecond())) {
+ // If the user is trying to target 1.7 but compiling with something older,
+ // the error message can be a bit misleading; instead point them in the
+ // direction of updating the project's build target.
+ Sdk currentSdk = Sdk.getCurrent();
+ if (currentSdk != null) {
+ IAndroidTarget target = currentSdk.getTarget(project.getProject());
+ if (target != null && target.getVersion().getApiLevel() < 19) {
+ errorMessage = "Using 1.7 requires compiling with Android 4.4 " +
+ "(KitKat); currently using " + target.getVersion();
+ }
+
+ ProjectState projectState = Sdk.getProjectState(project);
+ if (projectState != null) {
+ BuildToolInfo buildToolInfo = projectState.getBuildToolInfo();
+ if (buildToolInfo == null) {
+ buildToolInfo = currentSdk.getLatestBuildTool();
+ }
+ if (buildToolInfo != null && buildToolInfo.getRevision().getMajor() < 19) {
+ errorMessage = "Using 1.7 requires using Android Build Tools " +
+ "version 19 or later; currently using " +
+ buildToolInfo.getRevision();
+ }
+ }
+ }
+ }
+
+ markProject(AdtConstants.MARKER_ADT, errorMessage, IMarker.SEVERITY_ERROR);
+ AdtPlugin.printErrorToConsole(project, errorMessage);
+
+ return null;
+ }
+
+ // Check that the SDK directory has been setup.
+ String osSdkFolder = AdtPlugin.getOsSdkFolder();
+
+ if (osSdkFolder == null || osSdkFolder.length() == 0) {
+ AdtPlugin.printErrorToConsole(project, Messages.No_SDK_Setup_Error);
+ markProject(AdtConstants.MARKER_ADT, Messages.No_SDK_Setup_Error,
+ IMarker.SEVERITY_ERROR);
+
+ return null;
+ }
+
+ // check the 'gen' source folder is present
+ boolean hasGenSrcFolder = false; // whether the project has a 'gen' source folder setup
+
+ IClasspathEntry[] classpaths = javaProject.readRawClasspath();
+ if (classpaths != null) {
+ for (IClasspathEntry e : classpaths) {
+ if (e.getEntryKind() == IClasspathEntry.CPE_SOURCE) {
+ IPath path = e.getPath();
+ if (path.segmentCount() == 2 &&
+ path.segment(1).equals(SdkConstants.FD_GEN_SOURCES)) {
+ hasGenSrcFolder = true;
+ break;
+ }
+ }
+ }
+ }
+
+ boolean genFolderPresent = false; // whether the gen folder actually exists
+ IResource resource = project.findMember(SdkConstants.FD_GEN_SOURCES);
+ genFolderPresent = resource != null && resource.exists();
+
+ if (hasGenSrcFolder == false && genFolderPresent) {
+ // No source folder setup for 'gen' in the project, but there's already a
+ // 'gen' resource (file or folder).
+ String message;
+ if (resource.getType() == IResource.FOLDER) {
+ // folder exists already! This is an error. If the folder had been created
+ // by the NewProjectWizard, it'd be a source folder.
+ message = String.format("%1$s already exists but is not a source folder. Convert to a source folder or rename it.",
+ resource.getFullPath().toString());
+ } else {
+ // resource exists but is not a folder.
+ message = String.format(
+ "Resource %1$s is in the way. ADT needs a source folder called 'gen' to work. Rename or delete resource.",
+ resource.getFullPath().toString());
+ }
+
+ AdtPlugin.printErrorToConsole(project, message);
+ markProject(AdtConstants.MARKER_ADT, message, IMarker.SEVERITY_ERROR);
+
+ return null;
+ } else if (hasGenSrcFolder == false || genFolderPresent == false) {
+ // either there is no 'gen' source folder in the project (older SDK),
+ // or the folder does not exist (was deleted, or was a fresh svn checkout maybe.)
+
+ // In case we are migrating from an older SDK, we go through the current source
+ // folders and delete the generated Java files.
+ List<IPath> sourceFolders = BaseProjectHelper.getSourceClasspaths(javaProject);
+ IWorkspaceRoot root = ResourcesPlugin.getWorkspace().getRoot();
+ for (IPath path : sourceFolders) {
+ IResource member = root.findMember(path);
+ if (member != null) {
+ removeDerivedResources(member, monitor);
+ }
+ }
+
+ // create the new source folder, if needed
+ IFolder genFolder = project.getFolder(SdkConstants.FD_GEN_SOURCES);
+ if (genFolderPresent == false) {
+ AdtPlugin.printBuildToConsole(BuildVerbosity.VERBOSE, project,
+ "Creating 'gen' source folder for generated Java files");
+ genFolder.create(true /* force */, true /* local */,
+ new SubProgressMonitor(monitor, 10));
+ }
+
+ // add it to the source folder list, if needed only (or it will throw)
+ if (hasGenSrcFolder == false) {
+ IClasspathEntry[] entries = javaProject.getRawClasspath();
+ entries = ProjectHelper.addEntryToClasspath(entries,
+ JavaCore.newSourceEntry(genFolder.getFullPath()));
+ javaProject.setRawClasspath(entries, new SubProgressMonitor(monitor, 10));
+ }
+
+ // refresh specifically the gen folder first, as it may break the build
+ // if it doesn't arrive in time then refresh the whole project as usual.
+ genFolder.refreshLocal(IResource.DEPTH_ZERO, new SubProgressMonitor(monitor, 10));
+ project.refreshLocal(IResource.DEPTH_INFINITE, new SubProgressMonitor(monitor, 10));
+
+ // it seems like doing this fails to properly rebuild the project. the Java builder
+ // running right after this builder will not see the gen folder, and will not be
+ // restarted after this build. Therefore in this particular case, we start another
+ // build asynchronously so that it's rebuilt after this build.
+ launchJob(new Job("rebuild") {
+ @Override
+ protected IStatus run(IProgressMonitor m) {
+ try {
+ project.build(IncrementalProjectBuilder.INCREMENTAL_BUILD, m);
+ return Status.OK_STATUS;
+ } catch (CoreException e) {
+ return e.getStatus();
+ }
+ }
+ });
+
+ }
+
+ // convert older projects which use bin as the eclipse output folder into projects
+ // using bin/classes
+ IFolder androidOutput = BaseProjectHelper.getAndroidOutputFolder(project);
+ IFolder javaOutput = BaseProjectHelper.getJavaOutputFolder(project);
+ if (androidOutput.exists() == false || javaOutput == null ||
+ javaOutput.getParent().equals(androidOutput) == false) {
+ // get what we want as the new java output.
+ IFolder newJavaOutput = androidOutput.getFolder(SdkConstants.FD_CLASSES_OUTPUT);
+
+ if (androidOutput.exists() == false) {
+ androidOutput.create(true /*force*/, true /*local*/, monitor);
+ }
+
+ if (newJavaOutput.exists() == false) {
+ newJavaOutput.create(true /*force*/, true /*local*/, monitor);
+ }
+
+ // set the java output to this project.
+ javaProject.setOutputLocation(newJavaOutput.getFullPath(), monitor);
+
+ // need to do a full build. Can't build while we're already building, so launch a
+ // job to build it right after this build
+ launchJob(new Job("rebuild") {
+ @Override
+ protected IStatus run(IProgressMonitor jobMonitor) {
+ try {
+ project.build(IncrementalProjectBuilder.CLEAN_BUILD, jobMonitor);
+ return Status.OK_STATUS;
+ } catch (CoreException e) {
+ return e.getStatus();
+ }
+ }
+ });
+ }
+
+ // check that we have bin/res/
+ IFolder binResFolder = androidOutput.getFolder(SdkConstants.FD_RESOURCES);
+ if (binResFolder.exists() == false) {
+ binResFolder.create(true /* force */, true /* local */,
+ new SubProgressMonitor(monitor, 10));
+ project.refreshLocal(IResource.DEPTH_ONE, new SubProgressMonitor(monitor, 10));
+ }
+
+ // Check the preference to be sure we are supposed to refresh
+ // the folders.
+ if (AdtPrefs.getPrefs().getBuildForceResResfresh()) {
+ AdtPlugin.printBuildToConsole(BuildVerbosity.VERBOSE, project, Messages.Refreshing_Res);
+
+ // refresh the res folder.
+ IFolder resFolder = project.getFolder(AdtConstants.WS_RESOURCES);
+ resFolder.refreshLocal(IResource.DEPTH_INFINITE, monitor);
+
+ // Also refresh the assets folder to make sure the ApkBuilder
+ // will now it's changed and will force a new resource packaging.
+ IFolder assetsFolder = project.getFolder(AdtConstants.WS_ASSETS);
+ assetsFolder.refreshLocal(IResource.DEPTH_INFINITE, monitor);
+ }
+
+ return null;
+ }
+}