Android build library.
Xavier Ducrohet [Tue, 21 Aug 2012 01:07:58 +0000 (18:07 -0700)]
This library allows building Android libraries and
applications in the context of the Android SDK.

It is meant to be usable by various build systems and only
deals with the logic of building Android applications.
While it exposes some configurations and expect from
specific inputs, they are meant to be provided by the
build system itself.

It purposely doesn't include Eclipse or IDEA project
files due to dependencies that are downloaded from Maven
(and due to IDEA's tendency to put local path in its
project paths).
Instead those can be created using
    gradle eclipse
and
    gradle idea

The prebuilts are temporary till we move those libraries
to gradle or figure something else.

Change-Id: Ia95fa9ae3a619c2d9fe68b4cfa3ce72acfb4df3c

37 files changed:
builder/.gitignore [new file with mode: 0644]
builder/.settings/org.eclipse.jdt.core.prefs [new file with mode: 0644]
builder/.settings/org.moreunit.prefs [new file with mode: 0644]
builder/MODULE_LICENSE_APACHE2 [new file with mode: 0644]
builder/NOTICE [new file with mode: 0644]
builder/build.gradle [new file with mode: 0644]
builder/prebuilts/common.jar [new file with mode: 0644]
builder/prebuilts/manifmerger.jar [new file with mode: 0644]
builder/prebuilts/sdklib.jar [new file with mode: 0644]
builder/src/main/java/com/android/builder/AaptOptions.java [new file with mode: 0644]
builder/src/main/java/com/android/builder/AndroidBuilder.java [new file with mode: 0644]
builder/src/main/java/com/android/builder/AndroidDependency.java [new file with mode: 0644]
builder/src/main/java/com/android/builder/BuildConfigGenerator.java [new file with mode: 0644]
builder/src/main/java/com/android/builder/BuildType.java [new file with mode: 0644]
builder/src/main/java/com/android/builder/CommandLineRunner.java [new file with mode: 0644]
builder/src/main/java/com/android/builder/DefaultManifestParser.java [new file with mode: 0644]
builder/src/main/java/com/android/builder/DefaultSdkParser.java [new file with mode: 0644]
builder/src/main/java/com/android/builder/DexOptions.java [new file with mode: 0644]
builder/src/main/java/com/android/builder/JarDependency.java [new file with mode: 0644]
builder/src/main/java/com/android/builder/ManifestParser.java [new file with mode: 0644]
builder/src/main/java/com/android/builder/ProductFlavor.java [new file with mode: 0644]
builder/src/main/java/com/android/builder/SdkParser.java [new file with mode: 0644]
builder/src/main/java/com/android/builder/packaging/DuplicateFileException.java [new file with mode: 0644]
builder/src/main/java/com/android/builder/packaging/JavaResourceProcessor.java [new file with mode: 0644]
builder/src/main/java/com/android/builder/packaging/Packager.java [new file with mode: 0644]
builder/src/main/java/com/android/builder/packaging/PackagerException.java [new file with mode: 0644]
builder/src/main/java/com/android/builder/packaging/SealedPackageException.java [new file with mode: 0644]
builder/src/main/java/com/android/builder/signing/DebugKeyHelper.java [new file with mode: 0644]
builder/src/main/java/com/android/builder/signing/KeystoreHelper.java [new file with mode: 0644]
builder/src/main/java/com/android/builder/signing/KeytoolException.java [new file with mode: 0644]
builder/src/main/java/com/android/builder/signing/SignedJarBuilder.java [new file with mode: 0644]
builder/src/main/java/com/android/builder/signing/SigningInfo.java [new file with mode: 0644]
builder/src/main/resources/com/android/builder/BuildConfig.template [new file with mode: 0644]
builder/src/test/java/com/android/builder/AndroidBuilderTest.java [new file with mode: 0644]
builder/src/test/java/com/android/builder/BuildTypeTest.java [new file with mode: 0644]
builder/src/test/java/com/android/builder/ProductFlavorTest.java [new file with mode: 0644]
builder/src/test/java/com/android/builder/samples/Main.java [new file with mode: 0644]

diff --git a/builder/.gitignore b/builder/.gitignore
new file mode 100644 (file)
index 0000000..7466a16
--- /dev/null
@@ -0,0 +1,10 @@
+.gradle
+build
+builder.iml
+builder.ipr
+builder.iws
+.classpath
+.project
+bin
+
+
diff --git a/builder/.settings/org.eclipse.jdt.core.prefs b/builder/.settings/org.eclipse.jdt.core.prefs
new file mode 100644 (file)
index 0000000..7a52926
--- /dev/null
@@ -0,0 +1,100 @@
+#
+#Mon Aug 20 17:59:32 PDT 2012
+org.eclipse.jdt.core.compiler.problem.unusedParameterWhenImplementingAbstract=disabled
+org.eclipse.jdt.core.compiler.problem.comparingIdentical=warning
+org.eclipse.jdt.core.compiler.problem.unnecessaryElse=ignore
+org.eclipse.jdt.core.compiler.problem.missingDefaultCase=ignore
+org.eclipse.jdt.core.compiler.problem.redundantSuperinterface=warning
+org.eclipse.jdt.core.compiler.problem.unclosedCloseable=error
+org.eclipse.jdt.core.compiler.problem.reportMethodCanBePotentiallyStatic=ignore
+org.eclipse.jdt.core.compiler.problem.unusedLocal=warning
+org.eclipse.jdt.core.compiler.problem.emptyStatement=ignore
+org.eclipse.jdt.core.compiler.problem.unusedParameter=ignore
+org.eclipse.jdt.core.compiler.problem.unusedLabel=warning
+org.eclipse.jdt.core.compiler.problem.rawTypeReference=warning
+org.eclipse.jdt.core.compiler.problem.incompatibleNonInheritedInterfaceMethod=warning
+org.eclipse.jdt.core.compiler.debug.lineNumber=generate
+org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownException=warning
+org.eclipse.jdt.core.compiler.problem.missingOverrideAnnotationForInterfaceMethodImplementation=enabled
+org.eclipse.jdt.core.compiler.problem.nullAnnotationInferenceConflict=error
+org.eclipse.jdt.core.compiler.problem.unusedWarningToken=warning
+org.eclipse.jdt.core.compiler.problem.noImplicitStringConversion=warning
+org.eclipse.jdt.core.compiler.problem.deprecationInDeprecatedCode=disabled
+org.eclipse.jdt.core.compiler.problem.potentiallyUnclosedCloseable=warning
+org.eclipse.jdt.core.compiler.problem.deprecation=warning
+org.eclipse.jdt.core.compiler.problem.unusedParameterWhenOverridingConcrete=disabled
+org.eclipse.jdt.core.compiler.problem.redundantSpecificationOfTypeArguments=ignore
+org.eclipse.jdt.core.compiler.annotation.nonnull=com.android.annotations.NonNull
+org.eclipse.jdt.core.compiler.problem.missingSynchronizedOnInheritedMethod=ignore
+org.eclipse.jdt.core.compiler.problem.deprecationWhenOverridingDeprecatedMethod=disabled
+org.eclipse.jdt.core.compiler.problem.redundantNullCheck=ignore
+org.eclipse.jdt.core.compiler.problem.unusedImport=warning
+org.eclipse.jdt.core.compiler.annotation.nonnullbydefault=com.android.annotations.NonNullByDefault
+org.eclipse.jdt.core.compiler.annotation.nullable=com.android.annotations.Nullable
+org.eclipse.jdt.core.compiler.problem.suppressOptionalErrors=disabled
+org.eclipse.jdt.core.compiler.problem.suppressWarnings=enabled
+org.eclipse.jdt.core.compiler.problem.typeParameterHiding=warning
+org.eclipse.jdt.core.compiler.problem.localVariableHiding=warning
+org.eclipse.jdt.core.compiler.problem.possibleAccidentalBooleanAssignment=warning
+org.eclipse.jdt.core.compiler.problem.missingHashCodeMethod=warning
+org.eclipse.jdt.core.compiler.problem.includeNullInfoFromAsserts=enabled
+org.eclipse.jdt.core.compiler.problem.indirectStaticAccess=ignore
+org.eclipse.jdt.core.compiler.problem.potentialNullSpecViolation=error
+org.eclipse.jdt.core.compiler.problem.potentialNullReference=warning
+org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled
+org.eclipse.jdt.core.compiler.problem.finalParameterBound=warning
+org.eclipse.jdt.core.compiler.problem.missingSerialVersion=warning
+org.eclipse.jdt.core.compiler.problem.nullReference=error
+org.eclipse.jdt.core.compiler.source=1.6
+org.eclipse.jdt.core.compiler.annotation.missingNonNullByDefaultAnnotation=ignore
+org.eclipse.jdt.core.compiler.problem.enumIdentifier=error
+org.eclipse.jdt.core.compiler.problem.syntheticAccessEmulation=ignore
+org.eclipse.jdt.core.compiler.problem.fieldHiding=warning
+org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionExemptExceptionAndThrowable=enabled
+org.eclipse.jdt.core.compiler.problem.missingOverrideAnnotation=error
+org.eclipse.jdt.core.compiler.problem.hiddenCatchBlock=warning
+org.eclipse.jdt.core.compiler.problem.overridingPackageDefaultMethod=warning
+org.eclipse.jdt.core.compiler.problem.finallyBlockNotCompletingNormally=warning
+org.eclipse.jdt.core.compiler.problem.undocumentedEmptyBlock=ignore
+org.eclipse.jdt.core.compiler.problem.missingEnumCaseDespiteDefault=disabled
+org.eclipse.jdt.core.compiler.problem.methodWithConstructorName=warning
+org.eclipse.jdt.core.compiler.problem.unqualifiedFieldAccess=ignore
+org.eclipse.jdt.core.compiler.problem.discouragedReference=warning
+org.eclipse.jdt.core.compiler.problem.incompleteEnumSwitch=ignore
+eclipse.preferences.version=1
+org.eclipse.jdt.core.compiler.problem.unnecessaryTypeCheck=warning
+org.eclipse.jdt.core.compiler.problem.deadCode=warning
+org.eclipse.jdt.core.compiler.problem.specialParameterHidingField=disabled
+org.eclipse.jdt.core.compiler.problem.fatalOptionalError=enabled
+org.eclipse.jdt.core.compiler.problem.noEffectAssignment=warning
+org.eclipse.jdt.core.compiler.problem.nullSpecInsufficientInfo=warning
+org.eclipse.jdt.core.compiler.debug.sourceFile=generate
+org.eclipse.jdt.core.compiler.problem.nullSpecViolation=error
+org.eclipse.jdt.core.compiler.problem.missingDeprecatedAnnotation=warning
+org.eclipse.jdt.core.compiler.problem.unusedObjectAllocation=warning
+org.eclipse.jdt.core.compiler.problem.fallthroughCase=warning
+org.eclipse.jdt.core.compiler.annotation.nonnullisdefault=disabled
+org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.6
+org.eclipse.jdt.core.compiler.problem.unusedPrivateMember=warning
+org.eclipse.jdt.core.compiler.problem.redundantNullAnnotation=warning
+org.eclipse.jdt.core.compiler.problem.autoboxing=ignore
+org.eclipse.jdt.core.compiler.problem.explicitlyClosedAutoCloseable=ignore
+org.eclipse.jdt.core.compiler.problem.reportMethodCanBeStatic=ignore
+org.eclipse.jdt.core.compiler.problem.annotationSuperInterface=warning
+org.eclipse.jdt.core.compiler.problem.nonExternalizedStringLiteral=ignore
+org.eclipse.jdt.core.compiler.problem.unavoidableGenericTypeProblems=disabled
+org.eclipse.jdt.core.compiler.problem.parameterAssignment=ignore
+org.eclipse.jdt.core.compiler.problem.staticAccessReceiver=warning
+org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionIncludeDocCommentReference=enabled
+org.eclipse.jdt.core.compiler.problem.varargsArgumentNeedCast=warning
+org.eclipse.jdt.core.compiler.problem.assertIdentifier=error
+org.eclipse.jdt.core.compiler.compliance=1.6
+org.eclipse.jdt.core.compiler.problem.unusedParameterIncludeDocCommentReference=enabled
+org.eclipse.jdt.core.compiler.annotation.nullanalysis=enabled
+org.eclipse.jdt.core.compiler.debug.localVariable=generate
+org.eclipse.jdt.core.compiler.problem.unhandledWarningToken=warning
+org.eclipse.jdt.core.compiler.problem.forbiddenReference=error
+org.eclipse.jdt.core.compiler.codegen.unusedLocal=preserve
+org.eclipse.jdt.core.compiler.problem.uncheckedTypeOperation=warning
+org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionWhenOverriding=disabled
+org.eclipse.jdt.core.compiler.problem.nullUncheckedConversion=ignore
diff --git a/builder/.settings/org.moreunit.prefs b/builder/.settings/org.moreunit.prefs
new file mode 100644 (file)
index 0000000..c0ed4c1
--- /dev/null
@@ -0,0 +1,5 @@
+#Thu Jan 05 10:46:32 PST 2012
+eclipse.preferences.version=1
+org.moreunit.prefixes=
+org.moreunit.unitsourcefolder=common\:src\:common-tests\:src
+org.moreunit.useprojectsettings=true
diff --git a/builder/MODULE_LICENSE_APACHE2 b/builder/MODULE_LICENSE_APACHE2
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/builder/NOTICE b/builder/NOTICE
new file mode 100644 (file)
index 0000000..33ff961
--- /dev/null
@@ -0,0 +1,190 @@
+
+   Copyright (c) 2005-2012, The Android Open Source Project
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+
+   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.
+
+
+                                 Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS
+
diff --git a/builder/build.gradle b/builder/build.gradle
new file mode 100644 (file)
index 0000000..3d3265e
--- /dev/null
@@ -0,0 +1,14 @@
+apply plugin: 'java'
+apply plugin: 'eclipse'
+apply plugin: 'idea'
+
+repositories {
+       mavenCentral()
+}
+
+dependencies {
+       // this is temporary
+       compile fileTree(dir: 'prebuilts', include: '*.jar')
+       testCompile 'junit:junit:3.8.1'
+}
+
diff --git a/builder/prebuilts/common.jar b/builder/prebuilts/common.jar
new file mode 100644 (file)
index 0000000..29e054d
Binary files /dev/null and b/builder/prebuilts/common.jar differ
diff --git a/builder/prebuilts/manifmerger.jar b/builder/prebuilts/manifmerger.jar
new file mode 100644 (file)
index 0000000..db1ea77
Binary files /dev/null and b/builder/prebuilts/manifmerger.jar differ
diff --git a/builder/prebuilts/sdklib.jar b/builder/prebuilts/sdklib.jar
new file mode 100644 (file)
index 0000000..6dc2a17
Binary files /dev/null and b/builder/prebuilts/sdklib.jar differ
diff --git a/builder/src/main/java/com/android/builder/AaptOptions.java b/builder/src/main/java/com/android/builder/AaptOptions.java
new file mode 100644 (file)
index 0000000..360d204
--- /dev/null
@@ -0,0 +1,49 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.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.apache.org/licenses/LICENSE-2.0
+ *
+ * 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.builder;
+
+import com.android.annotations.NonNull;
+import com.android.annotations.Nullable;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class AaptOptions {
+
+    private String mIgnoreAssetsPattern;
+    private List<String> mNoCompressList;
+
+    public void setIgnoreAssetsPattern(@Nullable String ignoreAssetsPattern) {
+        mIgnoreAssetsPattern = ignoreAssetsPattern;
+    }
+
+    @Nullable public String getIgnoreAssetsPattern() {
+        return mIgnoreAssetsPattern;
+    }
+
+    public void addNoCompress(@NonNull String noCompress) {
+        if (mNoCompressList == null) {
+            mNoCompressList = new ArrayList<String>();
+        }
+
+        mNoCompressList.add(noCompress);
+    }
+
+    @Nullable public List<String> getNoCompressList() {
+        return mNoCompressList;
+    }
+}
diff --git a/builder/src/main/java/com/android/builder/AndroidBuilder.java b/builder/src/main/java/com/android/builder/AndroidBuilder.java
new file mode 100644 (file)
index 0000000..93035f7
--- /dev/null
@@ -0,0 +1,688 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.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.apache.org/licenses/LICENSE-2.0
+ *
+ * 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.builder;
+
+import com.android.annotations.NonNull;
+import com.android.annotations.Nullable;
+import com.android.annotations.VisibleForTesting;
+import com.android.builder.packaging.DuplicateFileException;
+import com.android.builder.packaging.JavaResourceProcessor;
+import com.android.builder.packaging.Packager;
+import com.android.builder.packaging.PackagerException;
+import com.android.builder.packaging.SealedPackageException;
+import com.android.builder.signing.DebugKeyHelper;
+import com.android.builder.signing.KeytoolException;
+import com.android.builder.signing.SigningInfo;
+import com.android.manifmerger.ManifestMerger;
+import com.android.manifmerger.MergerLog;
+import com.android.prefs.AndroidLocation.AndroidLocationException;
+import com.android.sdklib.IAndroidTarget;
+import com.android.sdklib.IAndroidTarget.IOptionalLibrary;
+import com.android.sdklib.io.FileOp;
+import com.android.utils.ILogger;
+
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * This is the main builder class. It is given all the data to process the build (such as
+ * {@link ProductFlavor}, {@link BuildType} and dependencies) and use them when doing specific
+ * build steps.
+ *
+ * To use:
+ * create a builder with {@link #AndroidBuilder(SdkParser, ILogger, boolean)},
+ * configure compile target with {@link #setTarget(String)}
+ * configure build variant with {@link #setBuildVariant(ProductFlavor, ProductFlavor, BuildType)},
+ * configure dependencies with {@link #setAndroidDependencies(List)} and
+ *     {@link #setJarDependencies(List)},
+ *
+ * then build steps can be done with
+ * {@link #generateBuildConfig(String, String)}
+ * {@link #mergeLibraryManifests(File, List, File)}
+ * {@link #processResources(String, String, String, String, String, String, String, String, String, String, String, AaptOptions)}
+ * {@link #convertBytecode(String, String, DexOptions)}
+ * {@link #packageApk(String, String, String, String, String, String, String)}
+ *
+ * Java compilation is not handled but the builder provides the runtime classpath with
+ * {@link #getRuntimeClasspath()}.
+ */
+public class AndroidBuilder {
+
+    private final SdkParser mSdkParser;
+    private final ILogger mLogger;
+    private final ManifestParser mManifestParser;
+    private final CommandLineRunner mCmdLineRunner;
+    private final boolean mVerboseExec;
+
+    private IAndroidTarget mTarget;
+
+    private ProductFlavor mProductFlavor;
+    private BuildType mBuildType;
+
+    private List<JarDependency> mJars;
+
+    /** List of direct library project dependencies. Each object defines its own dependencies. */
+    private final List<AndroidDependency> mDirectLibraryProjects =
+            new ArrayList<AndroidDependency>();
+    /** list of all library project dependencies in the flat list.
+     * The order is based on the order needed to call aapt: earlier libraries override resources
+     * of latter ones. */
+    private final List<AndroidDependency> mFlatLibraryProjects = new ArrayList<AndroidDependency>();
+
+    /**
+     * Creates an AndroidBuilder
+     * <p/>
+     * This receives an {@link SdkParser} to provide the build with information about the SDK, as
+     * well as an {@link ILogger} to display output.
+     * <p/>
+     * <var>verboseExec</var> is needed on top of the ILogger due to remote exec tools not being
+     * able to output info and verbose messages separately.
+     *
+     * @param sdkParser
+     * @param logger
+     * @param verboseExec
+     */
+    public AndroidBuilder(@NonNull SdkParser sdkParser, ILogger logger, boolean verboseExec) {
+        mSdkParser = sdkParser;
+        mLogger = logger;
+        mVerboseExec = verboseExec;
+        mManifestParser = new DefaultManifestParser();
+        mCmdLineRunner = new CommandLineRunner(mLogger);
+    }
+
+    @VisibleForTesting
+    AndroidBuilder(
+            @NonNull SdkParser sdkParser,
+            @NonNull ManifestParser manifestParser,
+            @NonNull CommandLineRunner cmdLineRunner,
+            @NonNull ILogger logger,
+            boolean verboseExec) {
+        mSdkParser = sdkParser;
+        mLogger = logger;
+        mVerboseExec = verboseExec;
+        mManifestParser = manifestParser;
+        mCmdLineRunner = cmdLineRunner;
+    }
+
+    /**
+     * Sets the compilation target hash string.
+     *
+     * @param target the compilation target
+     *
+     * @see IAndroidTarget#hashString()
+     */
+    public void setTarget(@NonNull String target) {
+        mTarget = mSdkParser.resolveTarget(target, mLogger);
+
+        if (mTarget == null) {
+            throw new RuntimeException("Unknown target: " + target);
+        }
+    }
+
+    /**
+     * Sets the build variant by providing the main and custom flavors and the build type
+     * @param mainFlavor
+     * @param productFlavor
+     * @param buildType
+     */
+    public void setBuildVariant(
+            @NonNull ProductFlavor mainFlavor,
+            @NonNull ProductFlavor productFlavor,
+            @NonNull BuildType buildType) {
+        mProductFlavor = productFlavor.mergeWith(mainFlavor);
+        mBuildType = buildType;
+    }
+
+    /**
+     * Returns the runtime classpath to be used during compilation.
+     */
+    public List<String> getRuntimeClasspath() {
+        if (mTarget == null) {
+            throw new IllegalArgumentException("Target not set.");
+        }
+
+        List<String> classpath = new ArrayList<String>();
+
+        classpath.add(mTarget.getPath(IAndroidTarget.ANDROID_JAR));
+
+        // add optional libraries if any
+        IOptionalLibrary[] libs = mTarget.getOptionalLibraries();
+        if (libs != null) {
+            for (IOptionalLibrary lib : libs) {
+                classpath.add(lib.getJarPath());
+            }
+        }
+
+        // add annotations.jar if needed.
+        if (mTarget.getVersion().getApiLevel() <= 15) {
+            classpath.add(mSdkParser.getAnnotationsJar());
+        }
+
+        return classpath;
+    }
+
+    public void setJarDependencies(List<JarDependency> jars) {
+        mJars = jars;
+    }
+
+    /**
+     * Set the Library Project dependencies.
+     * @param directLibraryProjects list of direct dependencies. Each library object should contain
+     *            its own dependencies.
+     */
+    public void setAndroidDependencies(List<AndroidDependency> directLibraryProjects) {
+        mDirectLibraryProjects.addAll(directLibraryProjects);
+        resolveIndirectLibraryDependencies(directLibraryProjects, mFlatLibraryProjects);
+    }
+
+    public void generateBuildConfig(
+            @NonNull String manifestLocation,
+            @NonNull String outGenLocation,
+            @Nullable List<String> additionalLines) throws IOException {
+        if (mProductFlavor == null || mBuildType == null) {
+            throw new IllegalArgumentException("No Product Flavor or Build Type set.");
+        }
+        if (mTarget == null) {
+            throw new IllegalArgumentException("Target not set.");
+        }
+
+        String packageName = getPackageOverride(manifestLocation);
+        if (packageName == null) {
+            packageName = getPackageFromManifest(manifestLocation);
+        }
+
+        BuildConfigGenerator generator = new BuildConfigGenerator(
+                outGenLocation, packageName, mBuildType.isDebuggable());
+        generator.generate(additionalLines);
+    }
+
+    public void preprocessResources(
+            @NonNull String mainResLocation,
+            @Nullable String flavorResLocation,
+            @Nullable String typeResLocation,
+            @NonNull String outResLocation) throws IOException, InterruptedException {
+        if (mProductFlavor == null || mBuildType == null) {
+            throw new IllegalArgumentException("No Product Flavor or Build Type set.");
+        }
+        if (mTarget == null) {
+            throw new IllegalArgumentException("Target not set.");
+        }
+
+        // launch aapt: create the command line
+        ArrayList<String> command = new ArrayList<String>();
+
+        @SuppressWarnings("deprecation")
+        String aaptPath = mTarget.getPath(IAndroidTarget.AAPT);
+
+        command.add(aaptPath);
+        command.add("crunch");
+
+        if (mVerboseExec) {
+            command.add("-v");
+        }
+
+        if (typeResLocation != null) {
+            command.add("-S");
+            command.add(typeResLocation);
+        }
+
+        if (flavorResLocation != null) {
+            command.add("-S");
+            command.add(flavorResLocation);
+        }
+
+        command.add("-S");
+        command.add(mainResLocation);
+
+        command.add("-C");
+        command.add(outResLocation);
+
+        mCmdLineRunner.runCmdLine(command);
+    }
+
+    public void mergeManifest(
+            @NonNull String mainLocation,
+            @Nullable String flavorLocation,
+            @Nullable String typeLocation,
+            @NonNull String outManifestLocation) {
+        if (mProductFlavor == null || mBuildType == null) {
+            throw new IllegalArgumentException("No Product Flavor or Build Type set.");
+        }
+        if (mTarget == null) {
+            throw new IllegalArgumentException("Target not set.");
+        }
+
+        try {
+            if (flavorLocation == null && typeLocation == null &&
+                    mFlatLibraryProjects.size() == 0) {
+                new FileOp().copyFile(new File(mainLocation), new File(outManifestLocation));
+            } else {
+                File appMergeOut;
+                if (mFlatLibraryProjects.size() == 0) {
+                    appMergeOut = new File(outManifestLocation);
+                } else {
+                    appMergeOut = File.createTempFile("manifestMerge", ".xml");
+                    appMergeOut.deleteOnExit();
+                }
+
+                List<File> manifests = new ArrayList<File>();
+                if (typeLocation != null) {
+                    manifests.add(new File(typeLocation));
+                }
+                if (flavorLocation != null) {
+                    manifests.add(new File(flavorLocation));
+                }
+
+                ManifestMerger merger = new ManifestMerger(MergerLog.wrapSdkLog(mLogger));
+                if (merger.process(
+                        appMergeOut,
+                        new File(mainLocation),
+                        manifests.toArray(new File[manifests.size()])) == false) {
+                    throw new RuntimeException();
+                }
+
+                if (mFlatLibraryProjects.size() > 0) {
+                    // recursively merge all manifests starting with the leaves and up toward the
+                    // root (the app)
+                    mergeLibraryManifests(appMergeOut, mDirectLibraryProjects,
+                            new File(outManifestLocation));
+                }
+            }
+        } catch (IOException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    private void mergeLibraryManifests(
+            File mainManifest,
+            List<AndroidDependency> libraries,
+            File outManifest) throws IOException {
+
+        List<File> manifests = new ArrayList<File>();
+        for (AndroidDependency library : libraries) {
+            List<AndroidDependency> subLibraries = library.getDependencies();
+            if (subLibraries == null || subLibraries.size() == 0) {
+                manifests.add(new File(library.getManifest()));
+            } else {
+                File mergeLibManifest = File.createTempFile("manifestMerge", ".xml");
+                mergeLibManifest.deleteOnExit();
+
+                mergeLibraryManifests(
+                        new File(library.getManifest()), subLibraries, mergeLibManifest);
+
+                manifests.add(mergeLibManifest);
+            }
+        }
+
+        ManifestMerger merger = new ManifestMerger(MergerLog.wrapSdkLog(mLogger));
+        if (merger.process(
+                outManifest,
+                mainManifest,
+                manifests.toArray(new File[manifests.size()])) == false) {
+            throw new RuntimeException();
+        }
+    }
+
+    public void processResources(
+            @NonNull String manifestLocation,
+            @NonNull String mainResLocation,
+            @Nullable String flavorResLocation,
+            @Nullable String typeResLocation,
+            @Nullable String crunchedResLocation,
+            @NonNull String mainAssetsLocation,
+            @Nullable String flavorAssetsLocation,
+            @Nullable String typeAssetsLocation,
+            @Nullable String outGenLocation,
+            @Nullable String outResPackageLocation,
+            @NonNull String outProguardLocation,
+            @NonNull AaptOptions options) throws IOException, InterruptedException {
+        if (mProductFlavor == null || mBuildType == null) {
+            throw new IllegalArgumentException("No Product Flavor or Build Type set.");
+        }
+        if (mTarget == null) {
+            throw new IllegalArgumentException("Target not set.");
+        }
+
+        // if both output types are empty, then there's nothing to do and this is an error
+        if (outGenLocation == null && outResPackageLocation == null) {
+            throw new IllegalArgumentException("no output provided for aapt task");
+        }
+
+        // launch aapt: create the command line
+        ArrayList<String> command = new ArrayList<String>();
+
+        @SuppressWarnings("deprecation")
+        String aaptPath = mTarget.getPath(IAndroidTarget.AAPT);
+
+        command.add(aaptPath);
+        command.add("package");
+
+        if (mVerboseExec) {
+            command.add("-v");
+        }
+
+        command.add("-f");
+        command.add("--no-crunch");
+
+
+        // inputs
+        command.add("-I");
+        command.add(mTarget.getPath(IAndroidTarget.ANDROID_JAR));
+
+        command.add("-M");
+        command.add(manifestLocation);
+
+        // TODO: handle libraries!
+        boolean useOverlay =  false;
+        if (crunchedResLocation != null) {
+            command.add("-S");
+            command.add(crunchedResLocation);
+            useOverlay = true;
+        }
+
+        if (typeResLocation != null) {
+            command.add("-S");
+            command.add(typeResLocation);
+            useOverlay = true;
+        }
+
+        if (flavorResLocation != null) {
+            command.add("-S");
+            command.add(flavorResLocation);
+            useOverlay = true;
+        }
+
+        command.add("-S");
+        command.add(mainResLocation);
+
+        if (useOverlay) {
+            command.add("--auto-add-overlay");
+        }
+
+        if (typeAssetsLocation != null) {
+            command.add("-A");
+            command.add(typeAssetsLocation);
+        }
+
+        if (flavorAssetsLocation != null) {
+            command.add("-A");
+            command.add(flavorAssetsLocation);
+        }
+
+        command.add("-A");
+        command.add(mainAssetsLocation);
+
+        // outputs
+
+        if (outGenLocation != null) {
+            command.add("-m");
+            command.add("-J");
+            command.add(outGenLocation);
+        }
+
+        if (outResPackageLocation != null) {
+            command.add("-F");
+            command.add(outResPackageLocation);
+
+            command.add("-G");
+            command.add(outProguardLocation);
+        }
+
+        // options controlled by build variants
+
+        if (mBuildType.isDebuggable()) {
+            command.add("--debug-mode");
+        }
+
+        String packageOverride = getPackageOverride(manifestLocation);
+        if (packageOverride != null) {
+            command.add("--rename-manifest-package");
+            command.add(packageOverride);
+            mLogger.verbose("Inserting package '%s' in AndroidManifest.xml", packageOverride);
+        }
+
+        boolean forceErrorOnReplace = false;
+
+        int versionCode = mProductFlavor.getVersionCode();
+        if (versionCode != -1) {
+            command.add("--version-code");
+            command.add(Integer.toString(versionCode));
+            mLogger.verbose("Inserting versionCode '%d' in AndroidManifest.xml", versionCode);
+            forceErrorOnReplace = true;
+        }
+
+        String versionName = mProductFlavor.getVersionName();
+        if (versionName != null) {
+            command.add("--version-name");
+            command.add(versionName);
+            mLogger.verbose("Inserting versionName '%s' in AndroidManifest.xml", versionName);
+            forceErrorOnReplace = true;
+        }
+
+        int minSdkVersion = mProductFlavor.getMinSdkVersion();
+        if (minSdkVersion != -1) {
+            command.add("--min-sdk-version");
+            command.add(Integer.toString(minSdkVersion));
+            mLogger.verbose("Inserting minSdkVersion '%d' in AndroidManifest.xml", minSdkVersion);
+            forceErrorOnReplace = true;
+        }
+
+        int targetSdkVersion = mProductFlavor.getTargetSdkVersion();
+        if (targetSdkVersion != -1) {
+            command.add("--target-sdk-version");
+            command.add(Integer.toString(targetSdkVersion));
+            mLogger.verbose("Inserting targetSdkVersion '%d' in AndroidManifest.xml",
+                    targetSdkVersion);
+            forceErrorOnReplace = true;
+        }
+
+        if (forceErrorOnReplace) {
+            // TODO: force aapt to fail if replace of versionCode/Name or min/targetSdkVersion fails
+            // Need to add the options to aapt first.
+        }
+
+        // AAPT options
+        if (options.getIgnoreAssetsPattern() != null) {
+            command.add("---ignore-assets");
+            command.add(options.getIgnoreAssetsPattern());
+        }
+
+        List<String> noCompressList = options.getNoCompressList();
+        if (noCompressList != null) {
+            for (String noCompress : noCompressList) {
+                command.add("0");
+                command.add(noCompress);
+            }
+        }
+
+        mCmdLineRunner.runCmdLine(command);
+    }
+
+    public void convertBytecode(
+            @NonNull String classesLocation,
+            @NonNull String outDexFile,
+            @NonNull DexOptions dexOptions) throws IOException, InterruptedException {
+        if (mProductFlavor == null || mBuildType == null) {
+            throw new IllegalArgumentException("No Product Flavor or Build Type set.");
+        }
+        if (mTarget == null) {
+            throw new IllegalArgumentException("Target not set.");
+        }
+
+        // launch dx: create the command line
+        ArrayList<String> command = new ArrayList<String>();
+
+        @SuppressWarnings("deprecation")
+        String dxPath = mTarget.getPath(IAndroidTarget.DX);
+        command.add(dxPath);
+
+        if (mVerboseExec) {
+            command.add("-v");
+        }
+
+        command.add("--output");
+        command.add(outDexFile);
+
+        // TODO: handle dependencies
+
+        mLogger.verbose("Input: " + classesLocation);
+
+        command.add(classesLocation);
+
+        mCmdLineRunner.runCmdLine(command);
+    }
+
+    /**
+     * Packages the apk.
+     * @param androidResPkgLocation
+     * @param classesDexLocation
+     * @param mainJavaResLocation
+     * @param flavorJavaResLocation
+     * @param buildTypeJavaResLocation
+     * @param jniLibsLocation
+     * @param outApkLocation
+     */
+    public void packageApk(
+            @NonNull String androidResPkgLocation,
+            @NonNull String classesDexLocation,
+            @NonNull String mainJavaResLocation,
+            @Nullable String flavorJavaResLocation,
+            @Nullable String buildTypeJavaResLocation,
+            @NonNull String jniLibsLocation,
+            @NonNull String outApkLocation) {
+        if (mProductFlavor == null || mBuildType == null) {
+            throw new IllegalArgumentException("No Product Flavor or Build Type set.");
+        }
+        if (mTarget == null) {
+            throw new IllegalArgumentException("Target not set.");
+        }
+
+        SigningInfo signingInfo = null;
+        if (mBuildType.isDebugSigningKey()) {
+            try {
+                String storeLocation = DebugKeyHelper.defaultDebugKeyStoreLocation();
+                File storeFile = new File(storeLocation);
+                if (storeFile.isDirectory()) {
+                    throw new RuntimeException(
+                            String.format("A folder is in the way of the debug keystore: %s",
+                                    storeLocation));
+                } else if (storeFile.exists() == false) {
+                    if (DebugKeyHelper.createNewStore(
+                            storeLocation, null /*storeType*/, mLogger) == false) {
+                        throw new RuntimeException();
+                    }
+                }
+
+                // load the key
+                signingInfo = DebugKeyHelper.getDebugKey(storeLocation, null /*storeStype*/);
+
+            } catch (AndroidLocationException e) {
+                throw new RuntimeException(e);
+            } catch (KeytoolException e) {
+                throw new RuntimeException(e);
+            } catch (FileNotFoundException e) {
+                // this shouldn't happen as we have checked ahead of calling getDebugKey.
+                throw new RuntimeException(e);
+            }
+        } else {
+            // todo: get the signing info from the flavor.
+        }
+
+        try {
+            Packager packager = new Packager(
+                    outApkLocation, androidResPkgLocation, classesDexLocation,
+                    signingInfo, mLogger);
+
+            packager.setDebugJniMode(mBuildType.isDebugJniBuild());
+
+            // figure out conflicts!
+            JavaResourceProcessor resProcessor = new JavaResourceProcessor(packager);
+            resProcessor.addSourceFolder(buildTypeJavaResLocation);
+            resProcessor.addSourceFolder(flavorJavaResLocation);
+            resProcessor.addSourceFolder(mainJavaResLocation);
+
+            // also add resources from library projects and jars
+
+            packager.addNativeLibraries(jniLibsLocation);
+
+            packager.sealApk();
+        } catch (PackagerException e) {
+            throw new RuntimeException(e);
+        } catch (DuplicateFileException e) {
+            throw new RuntimeException(e);
+        } catch (SealedPackageException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    @VisibleForTesting
+    String getPackageOverride(@NonNull String manifestLocation) {
+        String packageName = mProductFlavor.getPackageName();
+        String packageSuffix = mBuildType.getPackageNameSuffix();
+
+        if (packageSuffix != null) {
+            if (packageName == null) {
+                packageName = getPackageFromManifest(manifestLocation);
+            }
+
+            if (packageSuffix.charAt(0) == '.') {
+                packageName = packageName + packageSuffix;
+            } else {
+                packageName = packageName + '.' + packageSuffix;
+            }
+        }
+
+        return packageName;
+    }
+
+    @VisibleForTesting
+    String getPackageFromManifest(@NonNull String manifestLocation) {
+        return mManifestParser.getPackage(manifestLocation);
+    }
+
+    /**
+     * Resolves a given list of libraries, finds out if they depend on other libraries, and
+     * returns a flat list of all the direct and indirect dependencies in the proper order (first
+     * is higher priority when calling aapt).
+     * @param directDependencies the libraries to resolve
+     * @param outFlatDependencies where to store all the libraries.
+     */
+    @VisibleForTesting
+    void resolveIndirectLibraryDependencies(List<AndroidDependency> directDependencies,
+            List<AndroidDependency> outFlatDependencies) {
+        // loop in the inverse order to resolve dependencies on the libraries, so that if a library
+        // is required by two higher level libraries it can be inserted in the correct place
+        for (int i = directDependencies.size() - 1  ; i >= 0 ; i--) {
+            AndroidDependency library = directDependencies.get(i);
+
+            // get its libraries
+            List<AndroidDependency> dependencies = library.getDependencies();
+
+            // resolve the dependencies for those libraries
+            resolveIndirectLibraryDependencies(dependencies, outFlatDependencies);
+
+            // and add the current one (if needed) in front (higher priority)
+            if (outFlatDependencies.contains(library) == false) {
+                outFlatDependencies.add(0, library);
+            }
+        }
+    }
+
+}
diff --git a/builder/src/main/java/com/android/builder/AndroidDependency.java b/builder/src/main/java/com/android/builder/AndroidDependency.java
new file mode 100644 (file)
index 0000000..4c53d41
--- /dev/null
@@ -0,0 +1,66 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.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.apache.org/licenses/LICENSE-2.0
+ *
+ * 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.builder;
+
+import java.util.List;
+
+/**
+ * Represents a dependency on a Library Project.
+ */
+public interface AndroidDependency {
+
+    /**
+     * Returns the direct dependency of this dependency.
+     */
+    List<AndroidDependency> getDependencies();
+
+    /**
+     * Returns the location of the jar file to use for packaging.
+     * Cannot be null.
+     */
+    String getJarFile();
+
+    /**
+     * Returns the location of the manifest.
+     */
+    String getManifest();
+
+    /**
+     * Returns the location of the res folder.
+     */
+    String getResFolder();
+
+    /**
+     * Returns the location of the assets folder.
+     */
+    String getAssetsFolder();
+
+    /**
+     * Returns the location of the jni libraries folder.
+     */
+    String getJniFolder();
+
+    /**
+     * Returns the location of the proguard files.
+     */
+    String getProguardRules();
+
+    /**
+     * Returns the location of the lint jar.
+     */
+    String getLintJar();
+}
diff --git a/builder/src/main/java/com/android/builder/BuildConfigGenerator.java b/builder/src/main/java/com/android/builder/BuildConfigGenerator.java
new file mode 100644 (file)
index 0000000..3f5fa11
--- /dev/null
@@ -0,0 +1,168 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.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.apache.org/licenses/LICENSE-2.0
+ *
+ * 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.builder;
+
+import com.android.annotations.Nullable;
+
+import java.io.BufferedReader;
+import java.io.ByteArrayInputStream;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+
+/**
+ * Class able to generate a BuildConfig class in Android project.
+ * The BuildConfig class contains constants related to the build target.
+ */
+class BuildConfigGenerator {
+
+    public final static String BUILD_CONFIG_NAME = "BuildConfig.java";
+
+    private final static String PH_PACKAGE = "#PACKAGE#";
+    private final static String PH_DEBUG = "#DEBUG#";
+    private final static String PH_LINES = "#ADDITIONAL_LINES#";
+
+    private final String mGenFolder;
+    private final String mAppPackage;
+    private final boolean mDebug;
+
+    /**
+     * Creates a generator
+     * @param genFolder the gen folder of the project
+     * @param appPackage the application package
+     * @param debug whether it's a debug build
+     */
+    public BuildConfigGenerator(String genFolder, String appPackage, boolean debug) {
+        mGenFolder = genFolder;
+        mAppPackage = appPackage;
+        mDebug = debug;
+    }
+
+    /**
+     * Returns a File representing where the BuildConfig class will be.
+     */
+    public File getFolderPath() {
+        File genFolder = new File(mGenFolder);
+        return new File(genFolder, mAppPackage.replace('.', File.separatorChar));
+    }
+
+    public File getBuildConfigFile() {
+        File folder = getFolderPath();
+        return new File(folder, BUILD_CONFIG_NAME);
+    }
+
+    /**
+     * Generates the BuildConfig class.
+     * @param additionalLines a list of additional lines to be added to the class.
+     */
+    public void generate(@Nullable List<String> additionalLines) throws IOException {
+        String template = readEmbeddedTextFile("BuildConfig.template");
+
+        Map<String, String> map = new HashMap<String, String>();
+        map.put(PH_PACKAGE, mAppPackage);
+        map.put(PH_DEBUG, Boolean.toString(mDebug));
+
+        if (additionalLines != null) {
+            StringBuilder sb = new StringBuilder();
+            for (String line : additionalLines) {
+                sb.append("    ").append(line).append('\n');
+            }
+            map.put(PH_LINES, sb.toString());
+
+        } else {
+            map.put(PH_LINES, "");
+        }
+
+        String content = replaceParameters(template, map);
+
+        File pkgFolder = getFolderPath();
+        if (pkgFolder.isDirectory() == false) {
+            pkgFolder.mkdirs();
+        }
+
+        File buildConfigJava = new File(pkgFolder, BUILD_CONFIG_NAME);
+        writeFile(buildConfigJava, content);
+    }
+
+    /**
+     * Reads and returns the content of a text file embedded in the jar file.
+     * @param filepath the file path to the text file
+     * @return null if the file could not be read
+     * @throws IOException
+     */
+    private String readEmbeddedTextFile(String filepath) throws IOException {
+        InputStream is = BuildConfigGenerator.class.getResourceAsStream(filepath);
+        if (is != null) {
+            BufferedReader reader = new BufferedReader(new InputStreamReader(is));
+
+            String line;
+            StringBuilder total = new StringBuilder(reader.readLine());
+            while ((line = reader.readLine()) != null) {
+                total.append('\n');
+                total.append(line);
+            }
+
+            return total.toString();
+        }
+
+        // this really shouldn't happen unless the sdklib packaging is broken.
+        throw new IOException("BuildConfig template is missing!");
+    }
+
+    private void writeFile(File file, String content) throws IOException {
+        FileOutputStream fos = null;
+        try {
+            fos = new FileOutputStream(file);
+            InputStream source = new ByteArrayInputStream(content.getBytes("UTF-8"));
+
+            byte[] buffer = new byte[1024];
+            int count = 0;
+            while ((count = source.read(buffer)) != -1) {
+                fos.write(buffer, 0, count);
+            }
+        } finally {
+            if (fos != null) {
+                fos.close();
+            }
+        }
+    }
+
+    /**
+     * Replaces placeholders found in a string with values.
+     *
+     * @param str the string to search for placeholders.
+     * @param parameters a map of <placeholder, Value> to search for in the string
+     * @return A new String object with the placeholder replaced by the values.
+     */
+    private String replaceParameters(String str, Map<String, String> parameters) {
+
+        for (Entry<String, String> entry : parameters.entrySet()) {
+            String value = entry.getValue();
+            if (value != null) {
+                str = str.replaceAll(entry.getKey(), value);
+            }
+        }
+
+        return str;
+    }
+}
diff --git a/builder/src/main/java/com/android/builder/BuildType.java b/builder/src/main/java/com/android/builder/BuildType.java
new file mode 100644 (file)
index 0000000..596fa7b
--- /dev/null
@@ -0,0 +1,94 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.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.apache.org/licenses/LICENSE-2.0
+ *
+ * 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.builder;
+
+import com.android.annotations.NonNull;
+import com.android.annotations.Nullable;
+
+public class BuildType {
+
+    public final static String DEBUG = "debug";
+    public final static String RELEASE = "release";
+
+    private final String mName;
+    private boolean mDebuggable;
+    private boolean mDebugJniBuild;
+    private boolean mDebugSigningKey;
+    private String mPackageNameSuffix = null;
+
+    public BuildType(@NonNull String name) {
+        this.mName = name;
+        if (DEBUG.equals(name)) {
+            initDebug();
+        } else if (RELEASE.equals(name)) {
+            initRelease();
+        }
+    }
+
+    private void initDebug() {
+        mDebuggable = true;
+        mDebugJniBuild = true;
+        mDebugSigningKey = true;
+    }
+
+    private void initRelease() {
+        mDebuggable = false;
+        mDebugJniBuild = false;
+        mDebugSigningKey = false;
+    }
+
+    public String getName() {
+        return mName;
+    }
+
+    public void setDebuggable(boolean debuggable) {
+        mDebuggable = debuggable;
+    }
+
+    public boolean isDebuggable() {
+        return mDebuggable;
+    }
+
+    public void setDebugJniBuild(boolean debugJniBuild) {
+        mDebugJniBuild = debugJniBuild;
+    }
+
+    public boolean isDebugJniBuild() {
+        return mDebugJniBuild;
+    }
+
+    public void setDebugSigningKey(boolean debugSigningKey) {
+        mDebugSigningKey = debugSigningKey;
+    }
+
+    public boolean isDebugSigningKey() {
+        return mDebugSigningKey;
+    }
+
+    public void setPackageNameSuffix(@Nullable String packageNameSuffix) {
+        mPackageNameSuffix = packageNameSuffix;
+    }
+
+    @Nullable public String getPackageNameSuffix() {
+        return mPackageNameSuffix;
+    }
+
+    /*
+proguard enabled + rules
+Buildconfig: DEBUG flag + other custom properties?
+     */
+}
diff --git a/builder/src/main/java/com/android/builder/CommandLineRunner.java b/builder/src/main/java/com/android/builder/CommandLineRunner.java
new file mode 100644 (file)
index 0000000..99e55ad
--- /dev/null
@@ -0,0 +1,82 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.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.apache.org/licenses/LICENSE-2.0
+ *
+ * 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.builder;
+
+import com.android.annotations.Nullable;
+import com.android.sdklib.util.GrabProcessOutput;
+import com.android.sdklib.util.GrabProcessOutput.IProcessOutput;
+import com.android.sdklib.util.GrabProcessOutput.Wait;
+import com.android.utils.ILogger;
+
+import java.io.IOException;
+import java.util.List;
+
+class CommandLineRunner {
+
+    private final ILogger mLogger;
+
+    public CommandLineRunner(ILogger logger) {
+        mLogger = logger;
+    }
+
+    public void runCmdLine(List<String> command) throws IOException, InterruptedException {
+        String[] cmdArray = command.toArray(new String[command.size()]);
+        runCmdLine(cmdArray);
+    }
+
+    public void runCmdLine(String[] command) throws IOException, InterruptedException {
+        // launch the command line process
+        Process process = Runtime.getRuntime().exec(command);
+
+        // get the output and return code from the process
+        if (grabProcessOutput(process) != 0) {
+            throw new RuntimeException();
+        }
+    }
+
+    /**
+     * 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
+     */
+    private int grabProcessOutput(
+            final Process process)
+            throws InterruptedException {
+
+        return GrabProcessOutput.grabProcessOutput(
+                process,
+                Wait.WAIT_FOR_READERS, // we really want to make sure we get all the output!
+                new IProcessOutput() {
+
+                    @Override
+                    public void out(@Nullable String line) {
+                        if (line != null) {
+                            mLogger.info(line);
+                        }
+                    }
+
+                    @Override
+                    public void err(@Nullable String line) {
+                        if (line != null) {
+                            mLogger.error(null /*throwable*/, line);
+                        }
+                    }
+                });
+    }
+}
diff --git a/builder/src/main/java/com/android/builder/DefaultManifestParser.java b/builder/src/main/java/com/android/builder/DefaultManifestParser.java
new file mode 100644 (file)
index 0000000..74bf0d8
--- /dev/null
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.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.apache.org/licenses/LICENSE-2.0
+ *
+ * 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.builder;
+
+import com.android.xml.AndroidXPathFactory;
+
+import org.xml.sax.InputSource;
+
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+
+import javax.xml.xpath.XPath;
+import javax.xml.xpath.XPathExpressionException;
+
+public class DefaultManifestParser implements ManifestParser {
+
+    @Override
+    public String getPackage(String manifestFile) {
+        XPath xpath = AndroidXPathFactory.newXPath();
+
+        try {
+            return xpath.evaluate("/manifest/@package",
+                    new InputSource(new FileInputStream(manifestFile)));
+        } catch (XPathExpressionException e) {
+            // won't happen.
+        } catch (FileNotFoundException e) {
+            throw new RuntimeException(e);
+        }
+
+        return null;
+    }
+}
diff --git a/builder/src/main/java/com/android/builder/DefaultSdkParser.java b/builder/src/main/java/com/android/builder/DefaultSdkParser.java
new file mode 100644 (file)
index 0000000..ff8843d
--- /dev/null
@@ -0,0 +1,52 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.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.apache.org/licenses/LICENSE-2.0
+ *
+ * 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.builder;
+
+import com.android.SdkConstants;
+import com.android.annotations.NonNull;
+import com.android.sdklib.IAndroidTarget;
+import com.android.sdklib.SdkManager;
+import com.android.utils.ILogger;
+
+/**
+ * Default implementation of {@link SdkParser} for a normal Android SDK distribution.
+ */
+public class DefaultSdkParser implements SdkParser {
+
+    private final String mSdkLocation;
+
+    public DefaultSdkParser(@NonNull String sdkLocation) {
+        mSdkLocation = sdkLocation;
+    }
+
+    @Override
+    public IAndroidTarget resolveTarget(String target, ILogger logger) {
+        SdkManager manager = SdkManager.createManager(mSdkLocation, logger);
+        if (manager != null) {
+            return manager.getTargetFromHashString(target);
+        } else {
+            throw new RuntimeException("failed to parse SDK!");
+        }
+    }
+
+    @Override
+    public String getAnnotationsJar() {
+        return mSdkLocation + SdkConstants.FD_TOOLS +
+                '/' + SdkConstants.FD_SUPPORT +
+                '/' + SdkConstants.FN_ANNOTATIONS_JAR;
+    }
+}
diff --git a/builder/src/main/java/com/android/builder/DexOptions.java b/builder/src/main/java/com/android/builder/DexOptions.java
new file mode 100644 (file)
index 0000000..a686f39
--- /dev/null
@@ -0,0 +1,21 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.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.apache.org/licenses/LICENSE-2.0
+ *
+ * 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.builder;
+
+public class DexOptions {
+
+}
diff --git a/builder/src/main/java/com/android/builder/JarDependency.java b/builder/src/main/java/com/android/builder/JarDependency.java
new file mode 100644 (file)
index 0000000..91f71d8
--- /dev/null
@@ -0,0 +1,51 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.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.apache.org/licenses/LICENSE-2.0
+ *
+ * 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.builder;
+
+/**
+ * Represents a Jar dependency. This could be the output of a Java project.
+ */
+public class JarDependency {
+
+    private String mLocation;
+    private final boolean mCompiled;
+    private final boolean mPackaged;
+    private final boolean mProguarded;
+
+    public JarDependency(String location, boolean compiled, boolean packaged, boolean proguarded) {
+        mLocation = location;
+        mCompiled = compiled;
+        mPackaged = packaged;
+        mProguarded = proguarded;
+    }
+
+    public String getLocation() {
+        return mLocation;
+    }
+
+    public boolean isCompiled() {
+        return mCompiled;
+    }
+
+    public boolean isPackaged() {
+        return mPackaged;
+    }
+
+    public boolean isProguarded() {
+        return mProguarded;
+    }
+}
diff --git a/builder/src/main/java/com/android/builder/ManifestParser.java b/builder/src/main/java/com/android/builder/ManifestParser.java
new file mode 100644 (file)
index 0000000..47d16fd
--- /dev/null
@@ -0,0 +1,24 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.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.apache.org/licenses/LICENSE-2.0
+ *
+ * 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.builder;
+
+import com.android.annotations.NonNull;
+
+public interface ManifestParser {
+
+    String getPackage(@NonNull String manifestFile);
+}
diff --git a/builder/src/main/java/com/android/builder/ProductFlavor.java b/builder/src/main/java/com/android/builder/ProductFlavor.java
new file mode 100644 (file)
index 0000000..4d8cc0b
--- /dev/null
@@ -0,0 +1,132 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.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.apache.org/licenses/LICENSE-2.0
+ *
+ * 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.builder;
+
+import com.android.annotations.NonNull;
+
+public class ProductFlavor {
+
+    private final String mName;
+    private int mMinSdkVersion = -1;
+    private int mTargetSdkVersion = -1;
+    private int mVersionCode = -1;
+    private String mVersionName = null;
+    private String mPackageName = null;
+    private String mTestPackageName = null;
+    private String mTestInstrumentationRunner = null;
+
+    public ProductFlavor(String name) {
+        mName = name;
+    }
+
+    public String getName() {
+        return mName;
+    }
+
+    public void setPackageName(String packageName) {
+        mPackageName = packageName;
+    }
+
+    public String getPackageName() {
+        return mPackageName;
+    }
+
+    public void setVersionCode(int versionCode) {
+        mVersionCode = versionCode;
+    }
+
+    public int getVersionCode() {
+        return mVersionCode;
+    }
+
+    public void setVersionName(String versionName) {
+        mVersionName = versionName;
+    }
+
+    public String getVersionName() {
+        return mVersionName;
+    }
+
+    public void setMinSdkVersion(int minSdkVersion) {
+        mMinSdkVersion  = minSdkVersion;
+    }
+
+    public int getMinSdkVersion() {
+        return mMinSdkVersion;
+    }
+
+    public void setTargetSdkVersion(int targetSdkVersion) {
+        mTargetSdkVersion  = targetSdkVersion;
+    }
+
+    public int getTargetSdkVersion() {
+        return mTargetSdkVersion;
+    }
+
+    public void setTestPackageName(String testPackageName) {
+        mTestPackageName = testPackageName;
+    }
+
+    public String getTestPackageName() {
+        return mTestPackageName;
+    }
+
+    public void setTestInstrumentationRunner(String testInstrumentationRunner) {
+        mTestInstrumentationRunner = testInstrumentationRunner;
+    }
+
+    public String getTestInstrumentationRunner() {
+        return mTestInstrumentationRunner;
+    }
+
+    /**
+     * Merges the flavor on top of a base platform and returns a new object with the result.
+     * @param base
+     * @return
+     */
+    ProductFlavor mergeWith(@NonNull ProductFlavor base) {
+        ProductFlavor flavor = new ProductFlavor("");
+
+        flavor.mMinSdkVersion = chooseInt(mMinSdkVersion, base.mMinSdkVersion);
+        flavor.mTargetSdkVersion = chooseInt(mTargetSdkVersion, base.mTargetSdkVersion);
+
+        flavor.mVersionCode = chooseInt(mVersionCode, base.mVersionCode);
+        flavor.mVersionName = chooseString(mVersionName, base.mVersionName);
+
+        flavor.mPackageName = chooseString(mPackageName, base.mPackageName);
+
+        flavor.mTestPackageName = chooseString(mTestPackageName, base.mTestPackageName);
+        flavor.mTestInstrumentationRunner = chooseString(mTestInstrumentationRunner,
+                base.mTestInstrumentationRunner);
+
+        return flavor;
+    }
+
+    private int chooseInt(int overlay, int base) {
+        return overlay != -1 ? overlay : base;
+    }
+
+    private String chooseString(String overlay, String base) {
+        return overlay != null ? overlay : base;
+    }
+
+    /*
+    release signing info (keystore, key alias, passwords,...).
+    native abi filter
+*/
+
+}
diff --git a/builder/src/main/java/com/android/builder/SdkParser.java b/builder/src/main/java/com/android/builder/SdkParser.java
new file mode 100644 (file)
index 0000000..497d7a5
--- /dev/null
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.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.apache.org/licenses/LICENSE-2.0
+ *
+ * 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.builder;
+
+import com.android.annotations.NonNull;
+import com.android.sdklib.IAndroidTarget;
+import com.android.utils.ILogger;
+
+/**
+ * A parser able to parse the SDK and return valuable information to the build system.
+ *
+ */
+public interface SdkParser {
+
+    /**
+     * Resolves a target hash string and returns the corresponding {@link IAndroidTarget}
+     * @param target the target hash string.
+     * @param logger a logger object.
+     * @return the target or null if no match is found.
+     *
+     * @throws RuntimeException if the SDK cannot parsed.
+     *
+     * @see IAndroidTarget#hashString()
+     */
+    IAndroidTarget resolveTarget(@NonNull String target, @NonNull ILogger logger);
+
+    String getAnnotationsJar();
+
+}
diff --git a/builder/src/main/java/com/android/builder/packaging/DuplicateFileException.java b/builder/src/main/java/com/android/builder/packaging/DuplicateFileException.java
new file mode 100644 (file)
index 0000000..107422d
--- /dev/null
@@ -0,0 +1,55 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.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.apache.org/licenses/LICENSE-2.0
+ *
+ * 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.builder.packaging;
+
+import com.android.builder.signing.SignedJarBuilder.IZipEntryFilter.ZipAbortException;
+
+import java.io.File;
+
+/**
+ * An exception thrown during packaging of an APK file.
+ */
+public final class DuplicateFileException extends ZipAbortException {
+    private static final long serialVersionUID = 1L;
+    private final String mArchivePath;
+    private final File mFile1;
+    private final File mFile2;
+
+    public DuplicateFileException(String archivePath, File file1, File file2) {
+        super();
+        mArchivePath = archivePath;
+        mFile1 = file1;
+        mFile2 = file2;
+    }
+
+    public String getArchivePath() {
+        return mArchivePath;
+    }
+
+    public File getFile1() {
+        return mFile1;
+    }
+
+    public File getFile2() {
+        return mFile2;
+    }
+
+    @Override
+    public String getMessage() {
+        return "Duplicate files at the same path inside the APK";
+    }
+}
\ No newline at end of file
diff --git a/builder/src/main/java/com/android/builder/packaging/JavaResourceProcessor.java b/builder/src/main/java/com/android/builder/packaging/JavaResourceProcessor.java
new file mode 100644 (file)
index 0000000..e1e65fa
--- /dev/null
@@ -0,0 +1,186 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.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.apache.org/licenses/LICENSE-2.0
+ *
+ * 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.builder.packaging;
+
+
+import java.io.File;
+import java.io.IOException;
+
+public class JavaResourceProcessor {
+
+    private final IArchiveBuilder mBuilder;
+
+    public interface IArchiveBuilder {
+
+        /**
+         * Adds a file to the archive at a given path
+         * @param file the file to add
+         * @param archivePath the path of the file inside the APK archive.
+         * @throws PackagerException if an error occurred
+         * @throws SealedPackageException if the archive is already sealed.
+         * @throws DuplicateFileException if a file conflicts with another already added to the APK
+         *                                   at the same location inside the APK archive.
+         */
+        void addFile(File file, String archivePath) throws PackagerException,
+                SealedPackageException, DuplicateFileException;
+    }
+
+
+    public JavaResourceProcessor(IArchiveBuilder builder) {
+        mBuilder = builder;
+    }
+
+    /**
+     * Adds the resources from a source folder to a given {@link IArchiveBuilder}
+     * @param sourceLocation the source folder.
+     * @throws PackagerException if an error occurred
+     * @throws SealedPackageException if the APK is already sealed.
+     * @throws DuplicateFileException if a file conflicts with another already added to the APK
+     *                                   at the same location inside the APK archive.
+     */
+    public void addSourceFolder(String sourceLocation)
+            throws PackagerException, DuplicateFileException, SealedPackageException {
+        File sourceFolder = new File(sourceLocation);
+        if (sourceFolder.isDirectory()) {
+            try {
+                // file is a directory, process its content.
+                File[] files = sourceFolder.listFiles();
+                for (File file : files) {
+                    processFileForResource(file, null);
+                }
+            } catch (DuplicateFileException e) {
+                throw e;
+            } catch (SealedPackageException e) {
+                throw e;
+            } catch (Exception e) {
+                throw new PackagerException(e, "Failed to add %s", sourceFolder);
+            }
+        } else {
+            // not a directory? check if it's a file or doesn't exist
+            if (sourceFolder.exists()) {
+                throw new PackagerException("%s is not a folder", sourceFolder);
+            } else {
+                throw new PackagerException("%s does not exist", sourceFolder);
+            }
+        }
+    }
+
+
+    /**
+     * Processes a {@link File} that could be an APK {@link File}, or a folder containing
+     * java resources.
+     *
+     * @param file the {@link File} to process.
+     * @param path the relative path of this file to the source folder.
+     *          Can be <code>null</code> to identify a root file.
+     * @throws IOException
+     * @throws DuplicateFileException if a file conflicts with another already added
+     *          to the APK at the same location inside the APK archive.
+     * @throws PackagerException if an error occurred
+     * @throws SealedPackageException if the APK is already sealed.
+     */
+    private void processFileForResource(File file, String path)
+            throws IOException, DuplicateFileException, PackagerException, SealedPackageException {
+        if (file.isDirectory()) {
+            // a directory? we check it
+            if (checkFolderForPackaging(file.getName())) {
+                // if it's valid, we append its name to the current path.
+                if (path == null) {
+                    path = file.getName();
+                } else {
+                    path = path + "/" + file.getName();
+                }
+
+                // and process its content.
+                File[] files = file.listFiles();
+                for (File contentFile : files) {
+                    processFileForResource(contentFile, path);
+                }
+            }
+        } else {
+            // a file? we check it to make sure it should be added
+            if (checkFileForPackaging(file.getName())) {
+                // we append its name to the current path
+                if (path == null) {
+                    path = file.getName();
+                } else {
+                    path = path + "/" + file.getName();
+                }
+
+                // and add it to the apk
+                mBuilder.addFile(file, path);
+            }
+        }
+    }
+
+    /**
+     * Checks whether a folder and its content is valid for packaging into the .apk as
+     * standard Java resource.
+     * @param folderName the name of the folder.
+     */
+    public static boolean checkFolderForPackaging(String folderName) {
+        return folderName.equalsIgnoreCase("CVS") == false &&
+            folderName.equalsIgnoreCase(".svn") == false &&
+            folderName.equalsIgnoreCase("SCCS") == false &&
+            folderName.equalsIgnoreCase("META-INF") == false &&
+            folderName.startsWith("_") == false;
+    }
+
+    /**
+     * Checks a file to make sure it should be packaged as standard resources.
+     * @param fileName the name of the file (including extension)
+     * @return true if the file should be packaged as standard java resources.
+     */
+    public static boolean checkFileForPackaging(String fileName) {
+        String[] fileSegments = fileName.split("\\.");
+        String fileExt = "";
+        if (fileSegments.length > 1) {
+            fileExt = fileSegments[fileSegments.length-1];
+        }
+
+        return checkFileForPackaging(fileName, fileExt);
+    }
+
+    /**
+     * Checks a file to make sure it should be packaged as standard resources.
+     * @param fileName the name of the file (including extension)
+     * @param extension the extension of the file (excluding '.')
+     * @return true if the file should be packaged as standard java resources.
+     */
+    public static boolean checkFileForPackaging(String fileName, String extension) {
+        // ignore hidden files and backup files
+        if (fileName.charAt(0) == '.' || fileName.charAt(fileName.length()-1) == '~') {
+            return false;
+        }
+
+        return "aidl".equalsIgnoreCase(extension) == false &&       // Aidl files
+            "rs".equalsIgnoreCase(extension) == false &&            // RenderScript files
+            "rsh".equalsIgnoreCase(extension) == false &&           // RenderScript header files
+            "d".equalsIgnoreCase(extension) == false &&             // Dependency files
+            "java".equalsIgnoreCase(extension) == false &&          // Java files
+            "scala".equalsIgnoreCase(extension) == false &&         // Scala files
+            "class".equalsIgnoreCase(extension) == false &&         // Java class files
+            "scc".equalsIgnoreCase(extension) == false &&           // VisualSourceSafe
+            "swp".equalsIgnoreCase(extension) == false &&           // vi swap file
+            "thumbs.db".equalsIgnoreCase(fileName) == false &&      // image index file
+            "picasa.ini".equalsIgnoreCase(fileName) == false &&     // image index file
+            "package.html".equalsIgnoreCase(fileName) == false &&   // Javadoc
+            "overview.html".equalsIgnoreCase(fileName) == false;    // Javadoc
+    }
+
+
+}
diff --git a/builder/src/main/java/com/android/builder/packaging/Packager.java b/builder/src/main/java/com/android/builder/packaging/Packager.java
new file mode 100644 (file)
index 0000000..d60ee64
--- /dev/null
@@ -0,0 +1,563 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.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.apache.org/licenses/LICENSE-2.0
+ *
+ * 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.builder.packaging;
+
+import com.android.SdkConstants;
+import com.android.annotations.NonNull;
+import com.android.builder.packaging.JavaResourceProcessor.IArchiveBuilder;
+import com.android.builder.signing.SignedJarBuilder;
+import com.android.builder.signing.SignedJarBuilder.IZipEntryFilter;
+import com.android.builder.signing.SigningInfo;
+import com.android.sdklib.internal.build.DebugKeyProvider;
+import com.android.utils.ILogger;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.PrintStream;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.regex.Pattern;
+
+/**
+ * Class making the final app package.
+ * The inputs are:
+ * - packaged resources (output of aapt)
+ * - code file (ouput of dx)
+ * - Java resources coming from the project, its libraries, and its jar files
+ * - Native libraries from the project or its library.
+ *
+ */
+public final class Packager implements IArchiveBuilder {
+
+    private final static Pattern PATTERN_NATIVELIB_EXT = Pattern.compile("^.+\\.so$",
+            Pattern.CASE_INSENSITIVE);
+
+    /**
+     * A No-op zip filter. It's used to detect conflicts.
+     *
+     */
+    private final class NullZipFilter implements IZipEntryFilter {
+        private File mInputFile;
+
+        void reset(File inputFile) {
+            mInputFile = inputFile;
+        }
+
+        @Override
+        public boolean checkEntry(String archivePath) throws ZipAbortException {
+            mLogger.verbose("=> %s", archivePath);
+
+            File duplicate = checkFileForDuplicate(archivePath);
+            if (duplicate != null) {
+                throw new DuplicateFileException(archivePath, duplicate, mInputFile);
+            } else {
+                mAddedFiles.put(archivePath, mInputFile);
+            }
+
+            return true;
+        }
+    }
+
+    /**
+     * Custom {@link IZipEntryFilter} to filter out everything that is not a standard java
+     * resources, and also record whether the zip file contains native libraries.
+     * <p/>Used in {@link SignedJarBuilder#writeZip(java.io.InputStream, IZipEntryFilter)} when
+     * we only want the java resources from external jars.
+     */
+    private final class JavaAndNativeResourceFilter implements IZipEntryFilter {
+        private final List<String> mNativeLibs = new ArrayList<String>();
+        private boolean mNativeLibsConflict = false;
+        private File mInputFile;
+
+        @Override
+        public boolean checkEntry(String archivePath) throws ZipAbortException {
+            // split the path into segments.
+            String[] segments = archivePath.split("/");
+
+            // empty path? skip to next entry.
+            if (segments.length == 0) {
+                return false;
+            }
+
+            // Check each folders to make sure they should be included.
+            // Folders like CVS, .svn, etc.. should already have been excluded from the
+            // jar file, but we need to exclude some other folder (like /META-INF) so
+            // we check anyway.
+            for (int i = 0 ; i < segments.length - 1; i++) {
+                if (JavaResourceProcessor.checkFolderForPackaging(segments[i]) == false) {
+                    return false;
+                }
+            }
+
+            // get the file name from the path
+            String fileName = segments[segments.length-1];
+
+            boolean check = JavaResourceProcessor.checkFileForPackaging(fileName);
+
+            // only do additional checks if the file passes the default checks.
+            if (check) {
+                mLogger.verbose("=> %s", archivePath);
+
+                File duplicate = checkFileForDuplicate(archivePath);
+                if (duplicate != null) {
+                    throw new DuplicateFileException(archivePath, duplicate, mInputFile);
+                } else {
+                    mAddedFiles.put(archivePath, mInputFile);
+                }
+
+                if (archivePath.endsWith(".so")) {
+                    mNativeLibs.add(archivePath);
+
+                    // only .so located in lib/ will interfere with the installation
+                    if (archivePath.startsWith(SdkConstants.FD_APK_NATIVE_LIBS + "/")) {
+                        mNativeLibsConflict = true;
+                    }
+                } else if (archivePath.endsWith(".jnilib")) {
+                    mNativeLibs.add(archivePath);
+                }
+            }
+
+            return check;
+        }
+
+        List<String> getNativeLibs() {
+            return mNativeLibs;
+        }
+
+        boolean getNativeLibsConflict() {
+            return mNativeLibsConflict;
+        }
+
+        void reset(File inputFile) {
+            mInputFile = inputFile;
+            mNativeLibs.clear();
+            mNativeLibsConflict = false;
+        }
+    }
+
+    private SignedJarBuilder mBuilder = null;
+    private final ILogger mLogger;
+    private boolean mDebugJniMode = false;
+    private boolean mIsSealed = false;
+
+    private final NullZipFilter mNullFilter = new NullZipFilter();
+    private final JavaAndNativeResourceFilter mFilter = new JavaAndNativeResourceFilter();
+    private final HashMap<String, File> mAddedFiles = new HashMap<String, File>();
+
+    /**
+     * Status for the addition of a jar file resources into the APK.
+     * This indicates possible issues with native library inside the jar file.
+     */
+    public interface JarStatus {
+        /**
+         * Returns the list of native libraries found in the jar file.
+         */
+        List<String> getNativeLibs();
+
+        /**
+         * Returns whether some of those libraries were located in the location that Android
+         * expects its native libraries.
+         */
+        boolean hasNativeLibsConflicts();
+
+    }
+
+    /** Internal implementation of {@link JarStatus}. */
+    private final static class JarStatusImpl implements JarStatus {
+        public final List<String> mLibs;
+        public final boolean mNativeLibsConflict;
+
+        private JarStatusImpl(List<String> libs, boolean nativeLibsConflict) {
+            mLibs = libs;
+            mNativeLibsConflict = nativeLibsConflict;
+        }
+
+        @Override
+        public List<String> getNativeLibs() {
+            return mLibs;
+        }
+
+        @Override
+        public boolean hasNativeLibsConflicts() {
+            return mNativeLibsConflict;
+        }
+    }
+
+    /**
+     * Creates a new instance.
+     *
+     * This creates a new builder that will create the specified output file, using the two
+     * mandatory given input files.
+     *
+     * An optional debug keystore can be provided. If set, it is expected that the store password
+     * is 'android' and the key alias and password are 'androiddebugkey' and 'android'.
+     *
+     * An optional {@link PrintStream} can also be provided for verbose output. If null, there will
+     * be no output.
+     *
+     * @param apkLocation the file to create
+     * @param resLocation the file representing the packaged resource file.
+     * @param dexLocation the file representing the dex file. This can be null for apk with no code.
+     * @param signingInfo the signing information used to sign the package. Optional the OS path to the debug keystore, if needed or null.
+     * @param ILogger the logger.
+     * @throws PackagerException
+     */
+    public Packager(
+            @NonNull String apkLocation,
+            @NonNull String resLocation,
+            @NonNull String dexLocation,
+            SigningInfo signingInfo,
+            ILogger logger) throws PackagerException {
+
+        try {
+            File apkFile = new File(apkLocation);
+            checkOutputFile(apkFile);
+
+            File resFile = new File(resLocation);
+            checkInputFile(resFile);
+
+            File dexFile = null;
+            if (dexLocation != null) {
+                dexFile = new File(dexLocation);
+                checkInputFile(dexFile);
+            }
+
+            mLogger = logger;
+
+            mBuilder = new SignedJarBuilder(
+                    new FileOutputStream(apkFile, false /* append */),
+                    signingInfo.getKey(), signingInfo.getCertificate());
+
+            mLogger.verbose("Packaging %s", apkFile.getName());
+
+            // add the resources
+            addZipFile(resFile);
+
+            // add the class dex file at the root of the apk
+            if (dexFile != null) {
+                addFile(dexFile, SdkConstants.FN_APK_CLASSES_DEX);
+            }
+
+        } catch (PackagerException e) {
+            if (mBuilder != null) {
+                mBuilder.cleanUp();
+            }
+            throw e;
+        } catch (Exception e) {
+            if (mBuilder != null) {
+                mBuilder.cleanUp();
+            }
+            throw new PackagerException(e);
+        }
+    }
+
+    /**
+     * Sets the debug mode. In debug mode, when native libraries are present, the packaging
+     * will also include one or more copies of gdbserver in the final APK file.
+     *
+     * These are used for debugging native code, to ensure that gdbserver is accessible to the
+     * application.
+     *
+     * There will be one version of gdbserver for each ABI supported by the application.
+     *
+     * the gbdserver files are placed in the libs/abi/ folders automatically by the NDK.
+     *
+     * @param debugJniMode the debug-jni mode flag.
+     */
+    public void setDebugJniMode(boolean debugJniMode) {
+        mDebugJniMode = debugJniMode;
+    }
+
+    /**
+     * Adds a file to the APK at a given path
+     * @param file the file to add
+     * @param archivePath the path of the file inside the APK archive.
+     * @throws PackagerException if an error occurred
+     * @throws SealedPackageException if the APK is already sealed.
+     * @throws DuplicateFileException if a file conflicts with another already added to the APK
+     *                                   at the same location inside the APK archive.
+     */
+    @Override
+    public void addFile(File file, String archivePath) throws PackagerException,
+            SealedPackageException, DuplicateFileException {
+        if (mIsSealed) {
+            throw new SealedPackageException("APK is already sealed");
+        }
+
+        try {
+            doAddFile(file, archivePath);
+        } catch (DuplicateFileException e) {
+            mBuilder.cleanUp();
+            throw e;
+        } catch (Exception e) {
+            mBuilder.cleanUp();
+            throw new PackagerException(e, "Failed to add %s", file);
+        }
+    }
+
+    /**
+     * Adds the content from a zip file.
+     * All file keep the same path inside the archive.
+     * @param zipFile the zip File.
+     * @throws PackagerException if an error occurred
+     * @throws SealedPackageException if the APK is already sealed.
+     * @throws DuplicateFileException if a file conflicts with another already added to the APK
+     *                                   at the same location inside the APK archive.
+     */
+    void addZipFile(File zipFile) throws PackagerException, SealedPackageException,
+            DuplicateFileException {
+        if (mIsSealed) {
+            throw new SealedPackageException("APK is already sealed");
+        }
+
+        try {
+            mLogger.verbose("%s:", zipFile);
+
+            // reset the filter with this input.
+            mNullFilter.reset(zipFile);
+
+            // ask the builder to add the content of the file.
+            FileInputStream fis = new FileInputStream(zipFile);
+            mBuilder.writeZip(fis, mNullFilter);
+        } catch (DuplicateFileException e) {
+            mBuilder.cleanUp();
+            throw e;
+        } catch (Exception e) {
+            mBuilder.cleanUp();
+            throw new PackagerException(e, "Failed to add %s", zipFile);
+        }
+    }
+
+    /**
+     * Adds the resources from a jar file.
+     * @param jarFile the jar File.
+     * @return a {@link JarStatus} object indicating if native libraries where found in
+     *         the jar file.
+     * @throws PackagerException if an error occurred
+     * @throws SealedPackageException if the APK is already sealed.
+     * @throws DuplicateFileException if a file conflicts with another already added to the APK
+     *                                   at the same location inside the APK archive.
+     */
+    public JarStatus addResourcesFromJar(File jarFile) throws PackagerException,
+            SealedPackageException, DuplicateFileException {
+        if (mIsSealed) {
+            throw new SealedPackageException("APK is already sealed");
+        }
+
+        try {
+            mLogger.verbose("%s:", jarFile);
+
+            // reset the filter with this input.
+            mFilter.reset(jarFile);
+
+            // ask the builder to add the content of the file, filtered to only let through
+            // the java resources.
+            FileInputStream fis = new FileInputStream(jarFile);
+            mBuilder.writeZip(fis, mFilter);
+
+            // check if native libraries were found in the external library. This should
+            // constitutes an error or warning depending on if they are in lib/
+            return new JarStatusImpl(mFilter.getNativeLibs(), mFilter.getNativeLibsConflict());
+        } catch (DuplicateFileException e) {
+            mBuilder.cleanUp();
+            throw e;
+        } catch (Exception e) {
+            mBuilder.cleanUp();
+            throw new PackagerException(e, "Failed to add %s", jarFile);
+        }
+    }
+
+    /**
+     * Adds the native libraries from the top native folder.
+     * The content of this folder must be the various ABI folders.
+     *
+     * This may or may not copy gdbserver into the apk based on whether the debug mode is set.
+     *
+     * @param jniLibLocation the root folder containing the abi folders which contain the .so
+     *
+     * @throws PackagerException if an error occurred
+     * @throws SealedPackageException if the APK is already sealed.
+     * @throws DuplicateFileException if a file conflicts with another already added to the APK
+     *                                   at the same location inside the APK archive.
+     *
+     * @see #setDebugMode(boolean)
+     */
+    public void addNativeLibraries(String jniLibLocation)
+            throws PackagerException, SealedPackageException, DuplicateFileException {
+        if (mIsSealed) {
+            throw new SealedPackageException("APK is already sealed");
+        }
+
+        File nativeFolder = new File(jniLibLocation);
+
+        if (nativeFolder.isDirectory() == false) {
+            // not a directory? check if it's a file or doesn't exist
+            if (nativeFolder.exists()) {
+                throw new PackagerException("%s is not a folder", nativeFolder);
+            } else {
+                throw new PackagerException("%s does not exist", nativeFolder);
+            }
+        }
+
+        File[] abiList = nativeFolder.listFiles();
+
+        mLogger.verbose("Native folder: %s", nativeFolder);
+
+        if (abiList != null) {
+            for (File abi : abiList) {
+                if (abi.isDirectory()) { // ignore files
+
+                    File[] libs = abi.listFiles();
+                    if (libs != null) {
+                        for (File lib : libs) {
+                            // only consider files that are .so or, if in debug mode, that
+                            // are gdbserver executables
+                            if (lib.isFile() &&
+                                    (PATTERN_NATIVELIB_EXT.matcher(lib.getName()).matches() ||
+                                            (mDebugJniMode &&
+                                                    SdkConstants.FN_GDBSERVER.equals(
+                                                            lib.getName())))) {
+                                String path =
+                                    SdkConstants.FD_APK_NATIVE_LIBS + "/" +
+                                    abi.getName() + "/" + lib.getName();
+
+                                try {
+                                    doAddFile(lib, path);
+                                } catch (IOException e) {
+                                    mBuilder.cleanUp();
+                                    throw new PackagerException(e, "Failed to add %s", lib);
+                                }
+                            }
+                        }
+                    }
+                }
+            }
+        }
+    }
+
+    /**
+     * Seals the APK, and signs it if necessary.
+     * @throws PackagerException
+     * @throws PackagerException if an error occurred
+     * @throws SealedPackageException if the APK is already sealed.
+     */
+    public void sealApk() throws PackagerException, SealedPackageException {
+        if (mIsSealed) {
+            throw new SealedPackageException("APK is already sealed");
+        }
+
+        // close and sign the application package.
+        try {
+            mBuilder.close();
+            mIsSealed = true;
+        } catch (Exception e) {
+            throw new PackagerException(e, "Failed to seal APK");
+        } finally {
+            mBuilder.cleanUp();
+        }
+    }
+
+    private void doAddFile(File file, String archivePath) throws DuplicateFileException,
+            IOException {
+        mLogger.verbose("%1$s => %2$s", file, archivePath);
+
+        File duplicate = checkFileForDuplicate(archivePath);
+        if (duplicate != null) {
+            throw new DuplicateFileException(archivePath, duplicate, file);
+        }
+
+        mAddedFiles.put(archivePath, file);
+        mBuilder.writeFile(file, archivePath);
+    }
+
+    /**
+     * Checks if the given path in the APK archive has not already been used and if it has been,
+     * then returns a {@link File} object for the source of the duplicate
+     * @param archivePath the archive path to test.
+     * @return A File object of either a file at the same location or an archive that contains a
+     * file that was put at the same location.
+     */
+    private File checkFileForDuplicate(String archivePath) {
+        return mAddedFiles.get(archivePath);
+    }
+
+    /**
+     * Checks an output {@link File} object.
+     * This checks the following:
+     * - the file is not an existing directory.
+     * - if the file exists, that it can be modified.
+     * - if it doesn't exists, that a new file can be created.
+     * @param file the File to check
+     * @throws PackagerException If the check fails
+     */
+    private void checkOutputFile(File file) throws PackagerException {
+        if (file.isDirectory()) {
+            throw new PackagerException("%s is a directory!", file);
+        }
+
+        if (file.exists()) { // will be a file in this case.
+            if (file.canWrite() == false) {
+                throw new PackagerException("Cannot write %s", file);
+            }
+        } else {
+            try {
+                if (file.createNewFile() == false) {
+                    throw new PackagerException("Failed to create %s", file);
+                }
+            } catch (IOException e) {
+                throw new PackagerException(
+                        "Failed to create '%1$ss': %2$s", file, e.getMessage());
+            }
+        }
+    }
+
+    /**
+     * Checks an input {@link File} object.
+     * This checks the following:
+     * - the file is not an existing directory.
+     * - that the file exists (if <var>throwIfDoesntExist</var> is <code>false</code>) and can
+     *    be read.
+     * @param file the File to check
+     * @throws FileNotFoundException if the file is not here.
+     * @throws PackagerException If the file is a folder or a file that cannot be read.
+     */
+    private static void checkInputFile(File file) throws FileNotFoundException, PackagerException {
+        if (file.isDirectory()) {
+            throw new PackagerException("%s is a directory!", file);
+        }
+
+        if (file.exists()) {
+            if (file.canRead() == false) {
+                throw new PackagerException("Cannot read %s", file);
+            }
+        } else {
+            throw new FileNotFoundException(String.format("%s does not exist", file));
+        }
+    }
+
+    public static String getDebugKeystore() throws PackagerException {
+        try {
+            return DebugKeyProvider.getDefaultKeyStoreOsPath();
+        } catch (Exception e) {
+            throw new PackagerException(e, e.getMessage());
+        }
+    }
+}
diff --git a/builder/src/main/java/com/android/builder/packaging/PackagerException.java b/builder/src/main/java/com/android/builder/packaging/PackagerException.java
new file mode 100644 (file)
index 0000000..aa33c53
--- /dev/null
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.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.apache.org/licenses/LICENSE-2.0
+ *
+ * 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.builder.packaging;
+
+/**
+ * An exception thrown during packaging of an APK file.
+ */
+public final class PackagerException extends Exception {
+    private static final long serialVersionUID = 1L;
+
+    public PackagerException(String format, Object... args) {
+        super(String.format(format, args));
+    }
+
+    public PackagerException(Throwable cause, String format, Object... args) {
+        super(String.format(format, args), cause);
+    }
+
+    public PackagerException(Throwable cause) {
+        super(cause);
+    }
+}
\ No newline at end of file
diff --git a/builder/src/main/java/com/android/builder/packaging/SealedPackageException.java b/builder/src/main/java/com/android/builder/packaging/SealedPackageException.java
new file mode 100644 (file)
index 0000000..fd35b1e
--- /dev/null
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.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.apache.org/licenses/LICENSE-2.0
+ *
+ * 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.builder.packaging;
+
+/**
+ * An exception thrown when trying to add files to a sealed APK.
+ */
+public final class SealedPackageException extends Exception {
+    private static final long serialVersionUID = 1L;
+
+    public SealedPackageException(String format, Object... args) {
+        super(String.format(format, args));
+    }
+
+    public SealedPackageException(Throwable cause, String format, Object... args) {
+        super(String.format(format, args), cause);
+    }
+
+    public SealedPackageException(Throwable cause) {
+        super(cause);
+    }
+}
\ No newline at end of file
diff --git a/builder/src/main/java/com/android/builder/signing/DebugKeyHelper.java b/builder/src/main/java/com/android/builder/signing/DebugKeyHelper.java
new file mode 100644 (file)
index 0000000..72813b8
--- /dev/null
@@ -0,0 +1,75 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.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.apache.org/licenses/LICENSE-2.0
+ *
+ * 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.builder.signing;
+
+import com.android.annotations.NonNull;
+import com.android.prefs.AndroidLocation;
+import com.android.prefs.AndroidLocation.AndroidLocationException;
+import com.android.utils.ILogger;
+
+import java.io.FileNotFoundException;
+
+public class DebugKeyHelper {
+
+    private static final String PASSWORD_STRING = "android";
+    private static final String DEBUG_ALIAS = "AndroidDebugKey";
+
+    // Certificate CN value. This is a hard-coded value for the debug key.
+    // Android Market checks against this value in order to refuse applications signed with
+    // debug keys.
+    private static final String CERTIFICATE_DESC = "CN=Android Debug,O=Android,C=US";
+
+    /**
+     * Returns the location of the default debug keystore.
+     *
+     * @return The location of the default debug keystore.
+     * @throws AndroidLocationException if the location cannot be computed
+     */
+
+    public static String defaultDebugKeyStoreLocation() throws AndroidLocationException {
+        // this is guaranteed to either return a non null value (terminated with a platform
+        // specific separator), or throw.
+        String folder = AndroidLocation.getFolder();
+
+        return folder + "debug.keystore";
+    }
+
+    /**
+     * Creates a new store
+     * @param osKeyStorePath the location of the store
+     * @param storeType an optional keystore type, or <code>null</code> if the default is to
+     * be used.
+     * @param logger a logger object to receive the log of the creation.
+     * @throws KeytoolException
+     */
+    public static boolean createNewStore(@NonNull String keyStoreLocation,
+            String storeType, @NonNull ILogger logger) throws KeytoolException {
+
+        return KeystoreHelper.createNewStore(
+                keyStoreLocation, storeType, PASSWORD_STRING,
+                DEBUG_ALIAS, PASSWORD_STRING,
+                CERTIFICATE_DESC, 30 /* validity*/,
+                logger);
+    }
+
+    public static SigningInfo getDebugKey(@NonNull String keyStoreLocation, String storeStype)
+            throws KeytoolException, FileNotFoundException {
+
+        return KeystoreHelper.getSigningInfo(
+                keyStoreLocation, PASSWORD_STRING, storeStype, PASSWORD_STRING, PASSWORD_STRING);
+    }
+}
diff --git a/builder/src/main/java/com/android/builder/signing/KeystoreHelper.java b/builder/src/main/java/com/android/builder/signing/KeystoreHelper.java
new file mode 100644 (file)
index 0000000..69eb3f4
--- /dev/null
@@ -0,0 +1,196 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.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.apache.org/licenses/LICENSE-2.0
+ *
+ * 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.builder.signing;
+
+import com.android.annotations.NonNull;
+import com.android.annotations.Nullable;
+import com.android.sdklib.util.GrabProcessOutput;
+import com.android.sdklib.util.GrabProcessOutput.IProcessOutput;
+import com.android.sdklib.util.GrabProcessOutput.Wait;
+import com.android.utils.ILogger;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.security.KeyStore;
+import java.security.KeyStore.PrivateKeyEntry;
+import java.security.cert.X509Certificate;
+import java.util.ArrayList;
+
+/**
+ * A Helper to create new keystore/key.
+ */
+public final class KeystoreHelper {
+
+    /**
+     * Creates a new store
+     * @param osKeyStorePath the location of the store
+     * @param storeType an optional keystore type, or <code>null</code> if the default is to
+     * be used.
+     * @param storePassword
+     * @param alias
+     * @param keyPassword
+     * @param description
+     * @param validityYears
+     * @param logger
+     * @throws KeytoolException
+     */
+    public static boolean createNewStore(
+            @NonNull String osKeyStorePath,
+            String storeType,
+            @NonNull String storePassword,
+            @NonNull String alias,
+            @NonNull String keyPassword,
+            @NonNull String description,
+            int validityYears,
+            @NonNull final ILogger logger)
+            throws KeytoolException {
+
+        // get the executable name of keytool depending on the platform.
+        String os = System.getProperty("os.name");
+
+        String keytoolCommand;
+        if (os.startsWith("Windows")) {
+            keytoolCommand = "keytool.exe";
+        } else {
+            keytoolCommand = "keytool";
+        }
+
+        String javaHome = System.getProperty("java.home");
+
+        if (javaHome != null && javaHome.length() > 0) {
+            keytoolCommand = javaHome + File.separator + "bin" + File.separator + keytoolCommand;
+        }
+
+        // create the command line to call key tool to build the key with no user input.
+        ArrayList<String> commandList = new ArrayList<String>();
+        commandList.add(keytoolCommand);
+        commandList.add("-genkey");
+        commandList.add("-alias");
+        commandList.add(alias);
+        commandList.add("-keyalg");
+        commandList.add("RSA");
+        commandList.add("-dname");
+        commandList.add(description);
+        commandList.add("-validity");
+        commandList.add(Integer.toString(validityYears * 365));
+        commandList.add("-keypass");
+        commandList.add(keyPassword);
+        commandList.add("-keystore");
+        commandList.add(osKeyStorePath);
+        commandList.add("-storepass");
+        commandList.add(storePassword);
+        if (storeType != null) {
+            commandList.add("-storetype");
+            commandList.add(storeType);
+        }
+
+        String[] commandArray = commandList.toArray(new String[commandList.size()]);
+
+        // launch the command line process
+        int result = 0;
+        try {
+            Process process = Runtime.getRuntime().exec(commandArray);
+            result = GrabProcessOutput.grabProcessOutput(
+                    process,
+                    Wait.WAIT_FOR_READERS,
+                    new IProcessOutput() {
+                        @Override
+                        public void out(@Nullable String line) {
+                            if (line != null) {
+                                if (logger != null) {
+                                    logger.info(line);
+                                }
+                            }
+                        }
+
+                        @Override
+                        public void err(@Nullable String line) {
+                            if (line != null) {
+                                if (logger != null) {
+                                    logger.error(null /*throwable*/, line);
+                                }
+                            }
+                        }
+                    });
+        } catch (Exception e) {
+            // create the command line as one string for debugging purposes
+            StringBuilder builder = new StringBuilder();
+            boolean firstArg = true;
+            for (String arg : commandArray) {
+                boolean hasSpace = arg.indexOf(' ') != -1;
+
+                if (firstArg == true) {
+                    firstArg = false;
+                } else {
+                    builder.append(' ');
+                }
+
+                if (hasSpace) {
+                    builder.append('"');
+                }
+
+                builder.append(arg);
+
+                if (hasSpace) {
+                    builder.append('"');
+                }
+            }
+
+            throw new KeytoolException("Failed to create key: " + e.getMessage(),
+                    javaHome, builder.toString());
+        }
+
+        if (result != 0) {
+            return false;
+        }
+
+        return true;
+    }
+
+    public static SigningInfo getSigningInfo(
+            @NonNull String keyStoreLocation,
+            @NonNull String keyStorePassword,
+            String keyStoreType,
+            @NonNull String keyAlias,
+            @NonNull String keyPassword) throws KeytoolException, FileNotFoundException {
+
+        try {
+            KeyStore keyStore = KeyStore.getInstance(
+                    keyStoreType != null ? keyStoreType : KeyStore.getDefaultType());
+
+            FileInputStream fis = new FileInputStream(keyStoreLocation);
+            keyStore.load(fis, keyStorePassword.toCharArray());
+            fis.close();
+            PrivateKeyEntry entry = (KeyStore.PrivateKeyEntry)keyStore.getEntry(
+                    keyAlias, new KeyStore.PasswordProtection(keyPassword.toCharArray()));
+
+            if (entry != null) {
+                return new SigningInfo(entry.getPrivateKey(), (X509Certificate) entry.getCertificate());
+            }
+        } catch (FileNotFoundException e) {
+            throw e;
+        } catch (Exception e) {
+            throw new KeytoolException(
+                    String.format("Failed to read key %1$s from store \"%2$s\": %3$s",
+                            keyAlias, keyStoreLocation, e.getMessage()),
+                    e);
+        }
+
+        return null;
+    }
+}
diff --git a/builder/src/main/java/com/android/builder/signing/KeytoolException.java b/builder/src/main/java/com/android/builder/signing/KeytoolException.java
new file mode 100644 (file)
index 0000000..890096c
--- /dev/null
@@ -0,0 +1,48 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.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.apache.org/licenses/LICENSE-2.0
+ *
+ * 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.builder.signing;
+
+public class KeytoolException extends Exception {
+    /** default serial uid */
+    private static final long serialVersionUID = 1L;
+    private String mJavaHome = null;
+    private String mCommandLine = null;
+
+    KeytoolException(String message) {
+        super(message);
+    }
+
+    KeytoolException(String message, Throwable t) {
+        super(message, t);
+    }
+
+    KeytoolException(String message, String javaHome, String commandLine) {
+        super(message);
+
+        mJavaHome = javaHome;
+        mCommandLine = commandLine;
+    }
+
+    public String getJavaHome() {
+        return mJavaHome;
+    }
+
+    public String getCommandLine() {
+        return mCommandLine;
+    }
+
+}
diff --git a/builder/src/main/java/com/android/builder/signing/SignedJarBuilder.java b/builder/src/main/java/com/android/builder/signing/SignedJarBuilder.java
new file mode 100644 (file)
index 0000000..8bb042f
--- /dev/null
@@ -0,0 +1,388 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.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.apache.org/licenses/LICENSE-2.0
+ *
+ * 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.builder.signing;
+
+import com.android.builder.signing.SignedJarBuilder.IZipEntryFilter.ZipAbortException;
+
+import sun.misc.BASE64Encoder;
+import sun.security.pkcs.ContentInfo;
+import sun.security.pkcs.PKCS7;
+import sun.security.pkcs.SignerInfo;
+import sun.security.x509.AlgorithmId;
+import sun.security.x509.X500Name;
+
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FilterOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.PrintStream;
+import java.security.DigestOutputStream;
+import java.security.GeneralSecurityException;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.security.PrivateKey;
+import java.security.Signature;
+import java.security.SignatureException;
+import java.security.cert.X509Certificate;
+import java.util.Map;
+import java.util.jar.Attributes;
+import java.util.jar.JarEntry;
+import java.util.jar.JarFile;
+import java.util.jar.JarOutputStream;
+import java.util.jar.Manifest;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipInputStream;
+
+/**
+ * A Jar file builder with signature support.
+ */
+public class SignedJarBuilder {
+    private static final String DIGEST_ALGORITHM = "SHA1";
+    private static final String DIGEST_ATTR = "SHA1-Digest";
+    private static final String DIGEST_MANIFEST_ATTR = "SHA1-Digest-Manifest";
+
+    /** Write to another stream and also feed it to the Signature object. */
+    private static class SignatureOutputStream extends FilterOutputStream {
+        private Signature mSignature;
+        private int mCount = 0;
+
+        public SignatureOutputStream(OutputStream out, Signature sig) {
+            super(out);
+            mSignature = sig;
+        }
+
+        @Override
+        public void write(int b) throws IOException {
+            try {
+                mSignature.update((byte) b);
+            } catch (SignatureException e) {
+                throw new IOException("SignatureException: " + e);
+            }
+            super.write(b);
+            mCount++;
+        }
+
+        @Override
+        public void write(byte[] b, int off, int len) throws IOException {
+            try {
+                mSignature.update(b, off, len);
+            } catch (SignatureException e) {
+                throw new IOException("SignatureException: " + e);
+            }
+            super.write(b, off, len);
+            mCount += len;
+        }
+
+        public int size() {
+            return mCount;
+        }
+    }
+
+    private JarOutputStream mOutputJar;
+    private PrivateKey mKey;
+    private X509Certificate mCertificate;
+    private Manifest mManifest;
+    private BASE64Encoder mBase64Encoder;
+    private MessageDigest mMessageDigest;
+
+    private byte[] mBuffer = new byte[4096];
+
+    /**
+     * Classes which implement this interface provides a method to check whether a file should
+     * be added to a Jar file.
+     */
+    public interface IZipEntryFilter {
+
+        /**
+         * An exception thrown during packaging of a zip file into APK file.
+         * This is typically thrown by implementations of
+         * {@link IZipEntryFilter#checkEntry(String)}.
+         */
+        public static class ZipAbortException extends Exception {
+            private static final long serialVersionUID = 1L;
+
+            public ZipAbortException() {
+                super();
+            }
+
+            public ZipAbortException(String format, Object... args) {
+                super(String.format(format, args));
+            }
+
+            public ZipAbortException(Throwable cause, String format, Object... args) {
+                super(String.format(format, args), cause);
+            }
+
+            public ZipAbortException(Throwable cause) {
+                super(cause);
+            }
+        }
+
+
+        /**
+         * Checks a file for inclusion in a Jar archive.
+         * @param archivePath the archive file path of the entry
+         * @return <code>true</code> if the file should be included.
+         * @throws ZipAbortException if writing the file should be aborted.
+         */
+        public boolean checkEntry(String archivePath) throws ZipAbortException;
+    }
+
+    /**
+     * Creates a {@link SignedJarBuilder} with a given output stream, and signing information.
+     * <p/>If either <code>key</code> or <code>certificate</code> is <code>null</code> then
+     * the archive will not be signed.
+     * @param out the {@link OutputStream} where to write the Jar archive.
+     * @param key the {@link PrivateKey} used to sign the archive, or <code>null</code>.
+     * @param certificate the {@link X509Certificate} used to sign the archive, or
+     * <code>null</code>.
+     * @throws IOException
+     * @throws NoSuchAlgorithmException
+     */
+    public SignedJarBuilder(OutputStream out, PrivateKey key, X509Certificate certificate)
+            throws IOException, NoSuchAlgorithmException {
+        mOutputJar = new JarOutputStream(out);
+        mOutputJar.setLevel(9);
+        mKey = key;
+        mCertificate = certificate;
+
+        if (mKey != null && mCertificate != null) {
+            mManifest = new Manifest();
+            Attributes main = mManifest.getMainAttributes();
+            main.putValue("Manifest-Version", "1.0");
+            main.putValue("Created-By", "1.0 (Android)");
+
+            mBase64Encoder = new BASE64Encoder();
+            mMessageDigest = MessageDigest.getInstance(DIGEST_ALGORITHM);
+        }
+    }
+
+    /**
+     * Writes a new {@link File} into the archive.
+     * @param inputFile the {@link File} to write.
+     * @param jarPath the filepath inside the archive.
+     * @throws IOException
+     */
+    public void writeFile(File inputFile, String jarPath) throws IOException {
+        // Get an input stream on the file.
+        FileInputStream fis = new FileInputStream(inputFile);
+        try {
+
+            // create the zip entry
+            JarEntry entry = new JarEntry(jarPath);
+            entry.setTime(inputFile.lastModified());
+
+            writeEntry(fis, entry);
+        } finally {
+            // close the file stream used to read the file
+            fis.close();
+        }
+    }
+
+    /**
+     * Copies the content of a Jar/Zip archive into the receiver archive.
+     * <p/>An optional {@link IZipEntryFilter} allows to selectively choose which files
+     * to copy over.
+     * @param input the {@link InputStream} for the Jar/Zip to copy.
+     * @param filter the filter or <code>null</code>
+     * @throws IOException
+     * @throws ZipAbortException if the {@link IZipEntryFilter} filter indicated that the write
+     *                           must be aborted.
+     */
+    public void writeZip(InputStream input, IZipEntryFilter filter)
+            throws IOException, ZipAbortException {
+        ZipInputStream zis = new ZipInputStream(input);
+
+        try {
+            // loop on the entries of the intermediary package and put them in the final package.
+            ZipEntry entry;
+            while ((entry = zis.getNextEntry()) != null) {
+                String name = entry.getName();
+
+                // do not take directories or anything inside a potential META-INF folder.
+                if (entry.isDirectory() || name.startsWith("META-INF/")) {
+                    continue;
+                }
+
+                // if we have a filter, we check the entry against it
+                if (filter != null && filter.checkEntry(name) == false) {
+                    continue;
+                }
+
+                JarEntry newEntry;
+
+                // Preserve the STORED method of the input entry.
+                if (entry.getMethod() == JarEntry.STORED) {
+                    newEntry = new JarEntry(entry);
+                } else {
+                    // Create a new entry so that the compressed len is recomputed.
+                    newEntry = new JarEntry(name);
+                }
+
+                writeEntry(zis, newEntry);
+
+                zis.closeEntry();
+            }
+        } finally {
+            zis.close();
+        }
+    }
+
+    /**
+     * Closes the Jar archive by creating the manifest, and signing the archive.
+     * @throws IOException
+     * @throws GeneralSecurityException
+     */
+    public void close() throws IOException, GeneralSecurityException {
+        if (mManifest != null) {
+            // write the manifest to the jar file
+            mOutputJar.putNextEntry(new JarEntry(JarFile.MANIFEST_NAME));
+            mManifest.write(mOutputJar);
+
+            // CERT.SF
+            Signature signature = Signature.getInstance("SHA1with" + mKey.getAlgorithm());
+            signature.initSign(mKey);
+            mOutputJar.putNextEntry(new JarEntry("META-INF/CERT.SF"));
+            writeSignatureFile(new SignatureOutputStream(mOutputJar, signature));
+
+            // CERT.*
+            mOutputJar.putNextEntry(new JarEntry("META-INF/CERT." + mKey.getAlgorithm()));
+            writeSignatureBlock(signature, mCertificate, mKey);
+        }
+
+        mOutputJar.close();
+        mOutputJar = null;
+    }
+
+    /**
+     * Clean up of the builder for interrupted workflow.
+     * This does nothing if {@link #close()} was called successfully.
+     */
+    public void cleanUp() {
+        if (mOutputJar != null) {
+            try {
+                mOutputJar.close();
+            } catch (IOException e) {
+                // pass
+            }
+        }
+    }
+
+    /**
+     * Adds an entry to the output jar, and write its content from the {@link InputStream}
+     * @param input The input stream from where to write the entry content.
+     * @param entry the entry to write in the jar.
+     * @throws IOException
+     */
+    private void writeEntry(InputStream input, JarEntry entry) throws IOException {
+        // add the entry to the jar archive
+        mOutputJar.putNextEntry(entry);
+
+        // read the content of the entry from the input stream, and write it into the archive.
+        int count;
+        while ((count = input.read(mBuffer)) != -1) {
+            mOutputJar.write(mBuffer, 0, count);
+
+            // update the digest
+            if (mMessageDigest != null) {
+                mMessageDigest.update(mBuffer, 0, count);
+            }
+        }
+
+        // close the entry for this file
+        mOutputJar.closeEntry();
+
+        if (mManifest != null) {
+            // update the manifest for this entry.
+            Attributes attr = mManifest.getAttributes(entry.getName());
+            if (attr == null) {
+                attr = new Attributes();
+                mManifest.getEntries().put(entry.getName(), attr);
+            }
+            attr.putValue(DIGEST_ATTR, mBase64Encoder.encode(mMessageDigest.digest()));
+        }
+    }
+
+    /** Writes a .SF file with a digest to the manifest. */
+    private void writeSignatureFile(SignatureOutputStream out)
+            throws IOException, GeneralSecurityException {
+        Manifest sf = new Manifest();
+        Attributes main = sf.getMainAttributes();
+        main.putValue("Signature-Version", "1.0");
+        main.putValue("Created-By", "1.0 (Android)");
+
+        BASE64Encoder base64 = new BASE64Encoder();
+        MessageDigest md = MessageDigest.getInstance(DIGEST_ALGORITHM);
+        PrintStream print = new PrintStream(
+                new DigestOutputStream(new ByteArrayOutputStream(), md),
+                true, "UTF-8");
+
+        // Digest of the entire manifest
+        mManifest.write(print);
+        print.flush();
+        main.putValue(DIGEST_MANIFEST_ATTR, base64.encode(md.digest()));
+
+        Map<String, Attributes> entries = mManifest.getEntries();
+        for (Map.Entry<String, Attributes> entry : entries.entrySet()) {
+            // Digest of the manifest stanza for this entry.
+            print.print("Name: " + entry.getKey() + "\r\n");
+            for (Map.Entry<Object, Object> att : entry.getValue().entrySet()) {
+                print.print(att.getKey() + ": " + att.getValue() + "\r\n");
+            }
+            print.print("\r\n");
+            print.flush();
+
+            Attributes sfAttr = new Attributes();
+            sfAttr.putValue(DIGEST_ATTR, base64.encode(md.digest()));
+            sf.getEntries().put(entry.getKey(), sfAttr);
+        }
+
+        sf.write(out);
+
+        // A bug in the java.util.jar implementation of Android platforms
+        // up to version 1.6 will cause a spurious IOException to be thrown
+        // if the length of the signature file is a multiple of 1024 bytes.
+        // As a workaround, add an extra CRLF in this case.
+        if ((out.size() % 1024) == 0) {
+            out.write('\r');
+            out.write('\n');
+        }
+    }
+
+    /** Write the certificate file with a digital signature. */
+    private void writeSignatureBlock(Signature signature, X509Certificate publicKey,
+            PrivateKey privateKey)
+            throws IOException, GeneralSecurityException {
+        SignerInfo signerInfo = new SignerInfo(
+                new X500Name(publicKey.getIssuerX500Principal().getName()),
+                publicKey.getSerialNumber(),
+                AlgorithmId.get(DIGEST_ALGORITHM),
+                AlgorithmId.get(privateKey.getAlgorithm()),
+                signature.sign());
+
+        PKCS7 pkcs7 = new PKCS7(
+                new AlgorithmId[] { AlgorithmId.get(DIGEST_ALGORITHM) },
+                new ContentInfo(ContentInfo.DATA_OID, null),
+                new X509Certificate[] { publicKey },
+                new SignerInfo[] { signerInfo });
+
+        pkcs7.encodeSignedData(mOutputJar);
+    }
+}
diff --git a/builder/src/main/java/com/android/builder/signing/SigningInfo.java b/builder/src/main/java/com/android/builder/signing/SigningInfo.java
new file mode 100644 (file)
index 0000000..124e49d
--- /dev/null
@@ -0,0 +1,50 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.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.apache.org/licenses/LICENSE-2.0
+ *
+ * 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.builder.signing;
+
+import com.android.annotations.NonNull;
+
+import java.security.PrivateKey;
+import java.security.cert.X509Certificate;
+
+/**
+ * Signing information.
+ *
+ * Both the {@link PrivateKey} and the {@link X509Certificate} are guaranteed to be non-null.
+ *
+ */
+public class SigningInfo {
+    public final PrivateKey mKey;
+    public final X509Certificate mCertificate;
+
+    public SigningInfo(@NonNull PrivateKey key, @NonNull X509Certificate certificate) {
+        if (key == null || certificate == null) {
+            throw new IllegalArgumentException("key and certificate cannot be null");
+        }
+
+        mKey = key;
+        mCertificate = certificate;
+    }
+
+    public PrivateKey getKey() {
+        return mKey;
+    }
+
+    public X509Certificate getCertificate() {
+        return mCertificate;
+    }
+}
diff --git a/builder/src/main/resources/com/android/builder/BuildConfig.template b/builder/src/main/resources/com/android/builder/BuildConfig.template
new file mode 100644 (file)
index 0000000..618c013
--- /dev/null
@@ -0,0 +1,7 @@
+/** Automatically generated file. DO NOT MODIFY */
+package #PACKAGE#;
+
+public final class BuildConfig {
+    public final static boolean DEBUG = #DEBUG#;
+
+#ADDITIONAL_LINES#}
\ No newline at end of file
diff --git a/builder/src/test/java/com/android/builder/AndroidBuilderTest.java b/builder/src/test/java/com/android/builder/AndroidBuilderTest.java
new file mode 100644 (file)
index 0000000..40cc650
--- /dev/null
@@ -0,0 +1,110 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.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.apache.org/licenses/LICENSE-2.0
+ *
+ * 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.builder;
+
+import com.android.utils.StdLogger;
+
+import junit.framework.TestCase;
+
+public class AndroidBuilderTest extends TestCase {
+
+    private ProductFlavor mMain;
+    private ProductFlavor mFlavor;
+    private BuildType mDebug;
+
+    private static class ManifestParserMock implements ManifestParser {
+
+        private final String mPackageName;
+
+        ManifestParserMock(String packageName) {
+            mPackageName = packageName;
+        }
+
+        @Override
+        public String getPackage(String manifestFile) {
+            return mPackageName;
+        }
+    }
+
+    @Override
+    protected void setUp() throws Exception {
+        mMain = new ProductFlavor("main");
+        mFlavor = new ProductFlavor("flavor");
+        mDebug = new BuildType("debug");
+    }
+
+    public void testPackageOverrideNone() {
+        AndroidBuilder builder = new AndroidBuilder(new DefaultSdkParser(""),
+                new StdLogger(StdLogger.Level.ERROR), false /*verboseExec*/);
+
+        builder.setBuildVariant(mMain, mFlavor, mDebug);
+
+        assertNull(builder.getPackageOverride(""));
+    }
+
+    public void testPackageOverridePackageFromFlavor() {
+        AndroidBuilder builder = new AndroidBuilder(new DefaultSdkParser(""),
+                new StdLogger(StdLogger.Level.ERROR), false /*verboseExec*/);
+
+        mFlavor.setPackageName("foo.bar");
+
+        builder.setBuildVariant(mMain, mFlavor, mDebug);
+
+        assertEquals("foo.bar", builder.getPackageOverride(""));
+    }
+
+    public void testPackageOverridePackageFromFlavorWithSuffix() {
+        AndroidBuilder builder = new AndroidBuilder(new DefaultSdkParser(""),
+                new StdLogger(StdLogger.Level.ERROR), false /*verboseExec*/);
+
+        mFlavor.setPackageName("foo.bar");
+        mDebug.setPackageNameSuffix(".fortytwo");
+
+        builder.setBuildVariant(mMain, mFlavor, mDebug);
+
+        assertEquals("foo.bar.fortytwo", builder.getPackageOverride(""));
+    }
+
+    public void testPackageOverridePackageFromFlavorWithSuffix2() {
+        AndroidBuilder builder = new AndroidBuilder(new DefaultSdkParser(""),
+                new StdLogger(StdLogger.Level.ERROR), false /*verboseExec*/);
+
+        mFlavor.setPackageName("foo.bar");
+        mDebug.setPackageNameSuffix("fortytwo");
+
+        builder.setBuildVariant(mMain, mFlavor, mDebug);
+
+        assertEquals("foo.bar.fortytwo", builder.getPackageOverride(""));
+    }
+
+    public void testPackageOverridePackageWithSuffixOnly() {
+        StdLogger logger = new StdLogger(StdLogger.Level.ERROR);
+
+        AndroidBuilder builder = new AndroidBuilder(
+                new DefaultSdkParser(""),
+                new ManifestParserMock("fake.package.name"),
+                new CommandLineRunner(logger),
+                logger,
+                false /*verboseExec*/);
+
+        mDebug.setPackageNameSuffix("fortytwo");
+
+        builder.setBuildVariant(mMain, mFlavor, mDebug);
+
+        assertEquals("fake.package.name.fortytwo", builder.getPackageOverride(""));
+    }
+}
diff --git a/builder/src/test/java/com/android/builder/BuildTypeTest.java b/builder/src/test/java/com/android/builder/BuildTypeTest.java
new file mode 100644 (file)
index 0000000..1fecbb5
--- /dev/null
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.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.apache.org/licenses/LICENSE-2.0
+ *
+ * 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.builder;
+
+import junit.framework.TestCase;
+
+public class BuildTypeTest extends TestCase {
+
+    public void testDebug() {
+        BuildType type = new BuildType("debug");
+
+        assertTrue(type.isDebuggable());
+        assertTrue(type.isDebugJniBuild());
+        assertTrue(type.isDebugSigningKey());
+    }
+
+    public void testRelease() {
+        BuildType type = new BuildType("release");
+
+        assertFalse(type.isDebuggable());
+        assertFalse(type.isDebugJniBuild());
+        assertFalse(type.isDebugSigningKey());
+    }
+}
diff --git a/builder/src/test/java/com/android/builder/ProductFlavorTest.java b/builder/src/test/java/com/android/builder/ProductFlavorTest.java
new file mode 100644 (file)
index 0000000..84be8ee
--- /dev/null
@@ -0,0 +1,77 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.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.apache.org/licenses/LICENSE-2.0
+ *
+ * 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.builder;
+
+import junit.framework.TestCase;
+
+public class ProductFlavorTest extends TestCase {
+
+    private ProductFlavor mDefault;
+    private ProductFlavor mDefault2;
+    private ProductFlavor mCustom;
+
+    @Override
+    protected void setUp() throws Exception {
+        mDefault = new ProductFlavor("default");
+        mDefault2 = new ProductFlavor("default2");
+
+        mCustom = new ProductFlavor("custom");
+        mCustom.setMinSdkVersion(42);
+        mCustom.setTargetSdkVersion(43);
+        mCustom.setVersionCode(44);
+        mCustom.setVersionName("42.0");
+        mCustom.setPackageName("com.forty.two");
+        mCustom.setTestPackageName("com.forty.two.test");
+        mCustom.setTestInstrumentationRunner("com.forty.two.test.Runner");
+    }
+
+    public void testMergeOnDefault() {
+        ProductFlavor flavor = mCustom.mergeWith(mDefault);
+
+        assertEquals(42, flavor.getMinSdkVersion());
+        assertEquals(43, flavor.getTargetSdkVersion());
+        assertEquals(44, flavor.getVersionCode());
+        assertEquals("42.0", flavor.getVersionName());
+        assertEquals("com.forty.two", flavor.getPackageName());
+        assertEquals("com.forty.two.test", flavor.getTestPackageName());
+        assertEquals("com.forty.two.test.Runner", flavor.getTestInstrumentationRunner());
+    }
+
+    public void testMergeOnCustom() {
+        ProductFlavor flavor = mDefault.mergeWith(mCustom);
+
+        assertEquals(42, flavor.getMinSdkVersion());
+        assertEquals(43, flavor.getTargetSdkVersion());
+        assertEquals(44, flavor.getVersionCode());
+        assertEquals("42.0", flavor.getVersionName());
+        assertEquals("com.forty.two", flavor.getPackageName());
+        assertEquals("com.forty.two.test", flavor.getTestPackageName());
+        assertEquals("com.forty.two.test.Runner", flavor.getTestInstrumentationRunner());
+    }
+
+    public void testMergeDefaultOnDefault() {
+        ProductFlavor flavor = mDefault.mergeWith(mDefault2);
+
+        assertEquals(-1, flavor.getMinSdkVersion());
+        assertEquals(-1, flavor.getTargetSdkVersion());
+        assertEquals(-1, flavor.getVersionCode());
+        assertNull(flavor.getVersionName());
+        assertNull(flavor.getPackageName());
+        assertNull(flavor.getTestPackageName());
+        assertNull(flavor.getTestInstrumentationRunner());
+    }
+}
diff --git a/builder/src/test/java/com/android/builder/samples/Main.java b/builder/src/test/java/com/android/builder/samples/Main.java
new file mode 100644 (file)
index 0000000..2e53390
--- /dev/null
@@ -0,0 +1,89 @@
+package com.android.builder.samples;
+
+import com.android.builder.AaptOptions;
+import com.android.builder.AndroidBuilder;
+import com.android.builder.BuildType;
+import com.android.builder.DefaultSdkParser;
+import com.android.builder.ProductFlavor;
+import com.android.utils.StdLogger;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.Arrays;
+
+public class Main {
+
+    /**
+     * Usage: <sdklocation> <samplelocation>
+     *
+     *
+     * @param args
+     * @throws IOException
+     * @throws InterruptedException
+     */
+    public static void main(String[] args) throws IOException, InterruptedException {
+
+        DefaultSdkParser parser = new DefaultSdkParser(args[0]);
+
+        AndroidBuilder builder = new AndroidBuilder(parser,
+                new StdLogger(StdLogger.Level.VERBOSE), true);
+        builder.setTarget("android-15");
+
+        ProductFlavor mainFlavor = new ProductFlavor("main");
+        ProductFlavor customFlavor = new ProductFlavor("custom");
+        BuildType debug = new BuildType(BuildType.DEBUG);
+
+        customFlavor.setMinSdkVersion(15);
+        customFlavor.setTargetSdkVersion(16);
+
+        AaptOptions aaptOptions = new AaptOptions();
+
+        builder.setBuildVariant(mainFlavor, customFlavor, debug);
+
+        String sample = args[1];
+        String build = sample + File.separator + "build";
+        checkFolder(build);
+
+        String gen = build + File.separator + "gen";
+        checkFolder(gen);
+
+        String outRes = build + File.separator + "res";
+        checkFolder(outRes);
+
+        String[] lines = new String[] {
+                "public final static int A = 1;"
+        };
+        builder.generateBuildConfig(
+                sample + File.separator + "AndroidManifest.xml",
+                gen,
+                Arrays.asList(lines));
+
+        builder.preprocessResources(
+                sample + File.separator + "res",
+                null, /*flavorResLocation*/
+                null, /*typeResLocation*/
+                outRes);
+
+        builder.processResources(
+                sample + File.separator + "AndroidManifest.xml",
+                sample + File.separator + "res",
+                null, /*flavorResLocation*/
+                null, /*typeResLocation*/
+                outRes,
+                sample + File.separator + "assets",
+                null, /*flavorAssetsLocation*/
+                null, /*typeAssetsLocation*/
+                gen,
+                build + File.separator + "foo.apk_",
+                build + File.separator + "foo.proguard.txt",
+                aaptOptions);
+    }
+
+    private static void checkFolder(String path) {
+        File folder = new File(path);
+        if (folder.exists() == false) {
+            folder.mkdirs();
+        }
+    }
+
+}