aboutsummaryrefslogtreecommitdiff
path: root/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/project/ExportHelper.java
diff options
context:
space:
mode:
Diffstat (limited to 'eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/project/ExportHelper.java')
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/project/ExportHelper.java448
1 files changed, 448 insertions, 0 deletions
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/project/ExportHelper.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/project/ExportHelper.java
new file mode 100644
index 000000000..56e0c0938
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/project/ExportHelper.java
@@ -0,0 +1,448 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.internal.project;
+
+import static com.android.sdklib.internal.project.ProjectProperties.PROPERTY_SDK;
+
+import com.android.SdkConstants;
+import com.android.ide.eclipse.adt.AdtConstants;
+import com.android.ide.eclipse.adt.AdtPlugin;
+import com.android.ide.eclipse.adt.AndroidPrintStream;
+import com.android.ide.eclipse.adt.internal.build.BuildHelper;
+import com.android.ide.eclipse.adt.internal.build.DexException;
+import com.android.ide.eclipse.adt.internal.build.NativeLibInJarException;
+import com.android.ide.eclipse.adt.internal.build.ProguardExecException;
+import com.android.ide.eclipse.adt.internal.build.ProguardResultException;
+import com.android.ide.eclipse.adt.internal.preferences.AdtPrefs;
+import com.android.ide.eclipse.adt.internal.sdk.ProjectState;
+import com.android.ide.eclipse.adt.internal.sdk.Sdk;
+import com.android.ide.eclipse.adt.io.IFileWrapper;
+import com.android.sdklib.BuildToolInfo;
+import com.android.sdklib.build.ApkCreationException;
+import com.android.sdklib.build.DuplicateFileException;
+import com.android.sdklib.internal.project.ProjectProperties;
+import com.android.tools.lint.detector.api.LintUtils;
+import com.android.xml.AndroidManifest;
+
+import org.eclipse.core.resources.IFile;
+import org.eclipse.core.resources.IFolder;
+import org.eclipse.core.resources.IProject;
+import org.eclipse.core.resources.IResource;
+import org.eclipse.core.resources.IncrementalProjectBuilder;
+import org.eclipse.core.runtime.CoreException;
+import org.eclipse.core.runtime.IProgressMonitor;
+import org.eclipse.core.runtime.IStatus;
+import org.eclipse.core.runtime.Status;
+import org.eclipse.core.runtime.jobs.Job;
+import org.eclipse.jdt.core.IJavaProject;
+import org.eclipse.jdt.core.JavaCore;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.widgets.Display;
+import org.eclipse.swt.widgets.FileDialog;
+import org.eclipse.swt.widgets.Shell;
+
+import java.io.BufferedInputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.security.PrivateKey;
+import java.security.cert.X509Certificate;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.jar.JarEntry;
+import java.util.jar.JarOutputStream;
+
+/**
+ * Export helper to export release version of APKs.
+ */
+public final class ExportHelper {
+ private static final String HOME_PROPERTY = "user.home"; //$NON-NLS-1$
+ private static final String HOME_PROPERTY_REF = "${" + HOME_PROPERTY + '}'; //$NON-NLS-1$
+ private static final String SDK_PROPERTY_REF = "${" + PROPERTY_SDK + '}'; //$NON-NLS-1$
+ private final static String TEMP_PREFIX = "android_"; //$NON-NLS-1$
+
+ /**
+ * Exports a release version of the application created by the given project.
+ * @param project the project to export
+ * @param outputFile the file to write
+ * @param key the key to used for signing. Can be null.
+ * @param certificate the certificate used for signing. Can be null.
+ * @param monitor progress monitor
+ * @throws CoreException if an error occurs
+ */
+ public static void exportReleaseApk(IProject project, File outputFile, PrivateKey key,
+ X509Certificate certificate, IProgressMonitor monitor) throws CoreException {
+
+ // the export, takes the output of the precompiler & Java builders so it's
+ // important to call build in case the auto-build option of the workspace is disabled.
+ // Also enable dependency building to make sure everything is up to date.
+ // However do not package the APK since we're going to do it manually here, using a
+ // different output location.
+ ProjectHelper.compileInReleaseMode(project, monitor);
+
+ // if either key or certificate is null, ensure the other is null.
+ if (key == null) {
+ certificate = null;
+ } else if (certificate == null) {
+ key = null;
+ }
+
+ try {
+ // check if the manifest declares debuggable as true. While this is a release build,
+ // debuggable in the manifest will override this and generate a debug build
+ IResource manifestResource = project.findMember(SdkConstants.FN_ANDROID_MANIFEST_XML);
+ if (manifestResource.getType() != IResource.FILE) {
+ throw new CoreException(new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID,
+ String.format("%1$s missing.", SdkConstants.FN_ANDROID_MANIFEST_XML)));
+ }
+
+ IFileWrapper manifestFile = new IFileWrapper((IFile) manifestResource);
+ boolean debugMode = AndroidManifest.getDebuggable(manifestFile);
+
+ AndroidPrintStream fakeStream = new AndroidPrintStream(null, null, new OutputStream() {
+ @Override
+ public void write(int b) throws IOException {
+ // do nothing
+ }
+ });
+
+ ProjectState projectState = Sdk.getProjectState(project);
+
+ // get the jumbo mode option
+ String forceJumboStr = projectState.getProperty(AdtConstants.DEX_OPTIONS_FORCEJUMBO);
+ Boolean jumbo = Boolean.valueOf(forceJumboStr);
+
+ String dexMergerStr = projectState.getProperty(AdtConstants.DEX_OPTIONS_DISABLE_MERGER);
+ Boolean dexMerger = Boolean.valueOf(dexMergerStr);
+
+ BuildToolInfo buildToolInfo = getBuildTools(projectState);
+
+ BuildHelper helper = new BuildHelper(
+ projectState,
+ buildToolInfo,
+ fakeStream, fakeStream,
+ jumbo.booleanValue(),
+ dexMerger.booleanValue(),
+ debugMode, false /*verbose*/,
+ null /*resourceMarker*/);
+
+ // get the list of library projects
+ List<IProject> libProjects = projectState.getFullLibraryProjects();
+
+ // Step 1. Package the resources.
+
+ // tmp file for the packaged resource file. To not disturb the incremental builders
+ // output, all intermediary files are created in tmp files.
+ File resourceFile = File.createTempFile(TEMP_PREFIX, SdkConstants.DOT_RES);
+ resourceFile.deleteOnExit();
+
+ // Make sure the PNG crunch cache is up to date
+ helper.updateCrunchCache();
+
+ // get the merged manifest
+ IFolder androidOutputFolder = BaseProjectHelper.getAndroidOutputFolder(project);
+ IFile mergedManifestFile = androidOutputFolder.getFile(
+ SdkConstants.FN_ANDROID_MANIFEST_XML);
+
+
+ // package the resources.
+ helper.packageResources(
+ mergedManifestFile,
+ libProjects,
+ null, // res filter
+ 0, // versionCode
+ resourceFile.getParent(),
+ resourceFile.getName());
+
+ // Step 2. Convert the byte code to Dalvik bytecode
+
+ // tmp file for the packaged resource file.
+ File dexFile = File.createTempFile(TEMP_PREFIX, SdkConstants.DOT_DEX);
+ dexFile.deleteOnExit();
+
+ ProjectState state = Sdk.getProjectState(project);
+ String proguardConfig = state.getProperties().getProperty(
+ ProjectProperties.PROPERTY_PROGUARD_CONFIG);
+
+ boolean runProguard = false;
+ List<File> proguardConfigFiles = null;
+ if (proguardConfig != null && proguardConfig.length() > 0) {
+ // Be tolerant with respect to file and path separators just like
+ // Ant is. Allow "/" in the property file to mean whatever the file
+ // separator character is:
+ if (File.separatorChar != '/' && proguardConfig.indexOf('/') != -1) {
+ proguardConfig = proguardConfig.replace('/', File.separatorChar);
+ }
+
+ Iterable<String> paths = LintUtils.splitPath(proguardConfig);
+ for (String path : paths) {
+ if (path.startsWith(SDK_PROPERTY_REF)) {
+ path = AdtPrefs.getPrefs().getOsSdkFolder() +
+ path.substring(SDK_PROPERTY_REF.length());
+ } else if (path.startsWith(HOME_PROPERTY_REF)) {
+ path = System.getProperty(HOME_PROPERTY) +
+ path.substring(HOME_PROPERTY_REF.length());
+ }
+ File proguardConfigFile = new File(path);
+ if (proguardConfigFile.isAbsolute() == false) {
+ proguardConfigFile = new File(project.getLocation().toFile(), path);
+ }
+ if (proguardConfigFile.isFile()) {
+ if (proguardConfigFiles == null) {
+ proguardConfigFiles = new ArrayList<File>();
+ }
+ proguardConfigFiles.add(proguardConfigFile);
+ runProguard = true;
+ } else {
+ throw new CoreException(new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID,
+ "Invalid proguard configuration file path " + proguardConfigFile
+ + " does not exist or is not a regular file", null));
+ }
+ }
+
+ // get the proguard file output by aapt
+ if (proguardConfigFiles != null) {
+ IFile proguardFile = androidOutputFolder.getFile(AdtConstants.FN_AAPT_PROGUARD);
+ proguardConfigFiles.add(proguardFile.getLocation().toFile());
+ }
+ }
+
+ Collection<String> dxInput;
+
+ if (runProguard) {
+ // get all the compiled code paths. This will contain both project output
+ // folder and jar files.
+ Collection<String> paths = helper.getCompiledCodePaths();
+
+ // create a jar file containing all the project output (as proguard cannot
+ // process folders of .class files).
+ File inputJar = File.createTempFile(TEMP_PREFIX, SdkConstants.DOT_JAR);
+ inputJar.deleteOnExit();
+ JarOutputStream jos = new JarOutputStream(new FileOutputStream(inputJar));
+
+ // a list of the other paths (jar files.)
+ List<String> jars = new ArrayList<String>();
+
+ for (String path : paths) {
+ File root = new File(path);
+ if (root.isDirectory()) {
+ addFileToJar(jos, root, root);
+ } else if (root.isFile()) {
+ jars.add(path);
+ }
+ }
+ jos.close();
+
+ // destination file for proguard
+ File obfuscatedJar = File.createTempFile(TEMP_PREFIX, SdkConstants.DOT_JAR);
+ obfuscatedJar.deleteOnExit();
+
+ // run proguard
+ helper.runProguard(proguardConfigFiles, inputJar, jars, obfuscatedJar,
+ new File(project.getLocation().toFile(), SdkConstants.FD_PROGUARD));
+
+ helper.setProguardOutput(obfuscatedJar.getAbsolutePath());
+
+ // dx input is proguard's output
+ dxInput = Collections.singletonList(obfuscatedJar.getAbsolutePath());
+ } else {
+ // no proguard, simply get all the compiled code path: project output(s) +
+ // jar file(s)
+ dxInput = helper.getCompiledCodePaths();
+ }
+
+ IJavaProject javaProject = JavaCore.create(project);
+
+ helper.executeDx(javaProject, dxInput, dexFile.getAbsolutePath());
+
+ // Step 3. Final package
+
+ helper.finalPackage(
+ resourceFile.getAbsolutePath(),
+ dexFile.getAbsolutePath(),
+ outputFile.getAbsolutePath(),
+ libProjects,
+ key,
+ certificate,
+ null); //resourceMarker
+
+ // success!
+ } catch (CoreException e) {
+ throw e;
+ } catch (ProguardResultException e) {
+ String msg = String.format("Proguard returned with error code %d. See console",
+ e.getErrorCode());
+ AdtPlugin.printErrorToConsole(project, msg);
+ AdtPlugin.printErrorToConsole(project, (Object[]) e.getOutput());
+ throw new CoreException(new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID,
+ msg, e));
+ } catch (ProguardExecException e) {
+ String msg = String.format("Failed to run proguard: %s", e.getMessage());
+ AdtPlugin.printErrorToConsole(project, msg);
+ throw new CoreException(new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID,
+ msg, e));
+ } catch (DuplicateFileException e) {
+ String msg = String.format(
+ "Found duplicate file for APK: %1$s\nOrigin 1: %2$s\nOrigin 2: %3$s",
+ e.getArchivePath(), e.getFile1(), e.getFile2());
+ AdtPlugin.printErrorToConsole(project, msg);
+ throw new CoreException(new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID,
+ e.getMessage(), e));
+ } catch (NativeLibInJarException e) {
+ String msg = e.getMessage();
+
+ AdtPlugin.printErrorToConsole(project, msg);
+ AdtPlugin.printErrorToConsole(project, (Object[]) e.getAdditionalInfo());
+ throw new CoreException(new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID,
+ e.getMessage(), e));
+ } catch (DexException e) {
+ throw new CoreException(new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID,
+ e.getMessage(), e));
+ } catch (ApkCreationException e) {
+ throw new CoreException(new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID,
+ e.getMessage(), e));
+ } catch (Exception e) {
+ throw new CoreException(new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID,
+ "Failed to export application", e));
+ } finally {
+ // move back to a debug build.
+ // By using a normal build, we'll simply rebuild the debug version, and let the
+ // builder decide whether to build the full package or not.
+ ProjectHelper.buildWithDeps(project, IncrementalProjectBuilder.FULL_BUILD, monitor);
+ project.refreshLocal(IResource.DEPTH_INFINITE, monitor);
+ }
+ }
+
+ public static BuildToolInfo getBuildTools(ProjectState projectState)
+ throws CoreException {
+ BuildToolInfo buildToolInfo = projectState.getBuildToolInfo();
+ if (buildToolInfo == null) {
+ buildToolInfo = Sdk.getCurrent().getLatestBuildTool();
+ }
+
+ if (buildToolInfo == null) {
+ throw new CoreException(new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID,
+ "No Build Tools installed in the SDK."));
+ }
+ return buildToolInfo;
+ }
+
+ /**
+ * Exports an unsigned release APK after prompting the user for a location.
+ *
+ * <strong>Must be called from the UI thread.</strong>
+ *
+ * @param project the project to export
+ */
+ public static void exportUnsignedReleaseApk(final IProject project) {
+ Shell shell = Display.getCurrent().getActiveShell();
+
+ // create a default file name for the apk.
+ String fileName = project.getName() + SdkConstants.DOT_ANDROID_PACKAGE;
+
+ // Pop up the file save window to get the file location
+ FileDialog fileDialog = new FileDialog(shell, SWT.SAVE);
+
+ fileDialog.setText("Export Project");
+ fileDialog.setFileName(fileName);
+
+ final String saveLocation = fileDialog.open();
+ if (saveLocation != null) {
+ new Job("Android Release Export") {
+ @Override
+ protected IStatus run(IProgressMonitor monitor) {
+ try {
+ exportReleaseApk(project,
+ new File(saveLocation),
+ null, //key
+ null, //certificate
+ monitor);
+
+ // this is unsigned export. Let's tell the developers to run zip align
+ AdtPlugin.displayWarning("Android IDE Plug-in", String.format(
+ "An unsigned package of the application was saved at\n%1$s\n\n" +
+ "Before publishing the application you will need to:\n" +
+ "- Sign the application with your release key,\n" +
+ "- run zipalign on the signed package. ZipAlign is located in <SDK>/tools/\n\n" +
+ "Aligning applications allows Android to use application resources\n" +
+ "more efficiently.", saveLocation));
+
+ return Status.OK_STATUS;
+ } catch (CoreException e) {
+ AdtPlugin.displayError("Android IDE Plug-in", String.format(
+ "Error exporting application:\n\n%1$s", e.getMessage()));
+ return e.getStatus();
+ }
+ }
+ }.schedule();
+ }
+ }
+
+ /**
+ * Adds a file to a jar file.
+ * The <var>rootDirectory</var> dictates the path of the file inside the jar file. It must be
+ * a parent of <var>file</var>.
+ * @param jar the jar to add the file to
+ * @param file the file to add
+ * @param rootDirectory the rootDirectory.
+ * @throws IOException
+ */
+ private static void addFileToJar(JarOutputStream jar, File file, File rootDirectory)
+ throws IOException {
+ if (file.isDirectory()) {
+ if (file.getName().equals("META-INF") == false) {
+ for (File child: file.listFiles()) {
+ addFileToJar(jar, child, rootDirectory);
+ }
+ }
+ } else if (file.isFile()) {
+ String rootPath = rootDirectory.getAbsolutePath();
+ String path = file.getAbsolutePath();
+ path = path.substring(rootPath.length()).replace("\\", "/"); //$NON-NLS-1$ //$NON-NLS-2$
+ if (path.charAt(0) == '/') {
+ path = path.substring(1);
+ }
+
+ JarEntry entry = new JarEntry(path);
+ entry.setTime(file.lastModified());
+ jar.putNextEntry(entry);
+
+ // put the content of the file.
+ byte[] buffer = new byte[1024];
+ int count;
+ BufferedInputStream bis = null;
+ try {
+ bis = new BufferedInputStream(new FileInputStream(file));
+ while ((count = bis.read(buffer)) != -1) {
+ jar.write(buffer, 0, count);
+ }
+ } finally {
+ if (bis != null) {
+ try {
+ bis.close();
+ } catch (IOException ignore) {
+ }
+ }
+ }
+ jar.closeEntry();
+ }
+ }
+}