/*
* 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 com.android.ide.eclipse.adt.AdtPlugin;
import com.android.ide.eclipse.adt.AndroidConstants;
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.sdk.ProjectState;
import com.android.ide.eclipse.adt.internal.sdk.Sdk;
import com.android.ide.eclipse.adt.io.IFileWrapper;
import com.android.sdklib.SdkConstants;
import com.android.sdklib.build.ApkCreationException;
import com.android.sdklib.build.DuplicateFileException;
import com.android.sdklib.internal.project.ProjectProperties;
import com.android.sdklib.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.IPath;
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.jface.dialogs.MessageDialog;
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.List;
import java.util.jar.JarEntry;
import java.util.jar.JarOutputStream;
/**
* Export helper to export release version of APKs.
*/
public final class ExportHelper {
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
*/
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.
project.build(IncrementalProjectBuilder.INCREMENTAL_BUILD, 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
}
});
BuildHelper helper = new BuildHelper(project,
fakeStream, fakeStream,
debugMode, false /*verbose*/);
// get the list of library projects
ProjectState projectState = Sdk.getProjectState(project);
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, AndroidConstants.DOT_RES);
resourceFile.deleteOnExit();
// package the resources.
helper.packageResources(
project.getFile(SdkConstants.FN_ANDROID_MANIFEST_XML),
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, AndroidConstants.DOT_DEX);
dexFile.deleteOnExit();
ProjectState state = Sdk.getProjectState(project);
String proguardConfig = state.getProperties().getProperty(
ProjectProperties.PROPERTY_PROGUARD_CONFIG);
boolean runProguard = false;
File proguardConfigFile = null;
if (proguardConfig != null && proguardConfig.length() > 0) {
proguardConfigFile = new File(proguardConfig);
if (proguardConfigFile.isAbsolute() == false) {
proguardConfigFile = new File(project.getLocation().toFile(), proguardConfig);
}
runProguard = proguardConfigFile.isFile();
}
String[] dxInput;
if (runProguard) {
// the output of the main project (and any java-only project dependency)
String[] projectOutputs = helper.getProjectOutputs();
// create a jar from the output of these projects
File inputJar = File.createTempFile(TEMP_PREFIX, AndroidConstants.DOT_JAR);
inputJar.deleteOnExit();
JarOutputStream jos = new JarOutputStream(new FileOutputStream(inputJar));
for (String po : projectOutputs) {
File root = new File(po);
if (root.exists()) {
addFileToJar(jos, root, root);
}
}
jos.close();
// get the other jar files
String[] jarFiles = helper.getCompiledCodePaths(false /*includeProjectOutputs*/,
null /*resourceMarker*/);
// destination file for proguard
File obfuscatedJar = File.createTempFile(TEMP_PREFIX, AndroidConstants.DOT_JAR);
obfuscatedJar.deleteOnExit();
// run proguard
helper.runProguard(proguardConfigFile, inputJar, jarFiles, obfuscatedJar,
new File(project.getLocation().toFile(), SdkConstants.FD_PROGUARD));
// dx input is proguard's output
dxInput = new String[] { obfuscatedJar.getAbsolutePath() };
} else {
// no proguard, simply get all the compiled code path: project output(s) +
// jar file(s)
dxInput = helper.getCompiledCodePaths(true /*includeProjectOutputs*/,
null /*resourceMarker*/);
}
IJavaProject javaProject = JavaCore.create(project);
List<IProject> javaProjects = ProjectHelper.getReferencedProjects(project);
List<IJavaProject> referencedJavaProjects = BuildHelper.getJavaProjects(
javaProjects);
helper.executeDx(javaProject, dxInput, dexFile.getAbsolutePath());
// Step 3. Final package
helper.finalPackage(
resourceFile.getAbsolutePath(),
dexFile.getAbsolutePath(),
outputFile.getAbsolutePath(),
javaProject,
libProjects,
referencedJavaProjects,
null /*abiFilter*/,
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));
}
}
/**
* 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();
// get the java project to get the output directory
IFolder outputFolder = BaseProjectHelper.getOutputFolder(project);
if (outputFolder != null) {
IPath binLocation = outputFolder.getLocation();
// make the full path to the package
String fileName = project.getName() + AndroidConstants.DOT_ANDROID_PACKAGE;
File file = new File(binLocation.toOSString() + File.separator + fileName);
if (file.exists() == false || file.isFile() == false) {
MessageDialog.openError(Display.getCurrent().getActiveShell(),
"Android IDE Plug-in",
String.format("Failed to export %1$s: %2$s doesn't exist!",
project.getName(), file.getPath()));
return;
}
// ok now pop up the file save window
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();
}
} else {
MessageDialog.openError(shell, "Android IDE Plug-in",
String.format("Failed to export %1$s: Could not get project output location",
project.getName()));
}
}
/**
* 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()) {
for (File child: file.listFiles()) {
addFileToJar(jar, child, rootDirectory);
}
} else if (file.isFile()) {
// check the extension
String name = file.getName();
if (name.toLowerCase().endsWith(AndroidConstants.DOT_CLASS) == false) {
return;
}
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 = new BufferedInputStream(new FileInputStream(file));
while ((count = bis.read(buffer)) != -1) {
jar.write(buffer, 0, count);
}
jar.closeEntry();
}
}
}