/* * 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.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 b = Boolean.valueOf(forceJumboStr); BuildHelper helper = new BuildHelper(project, fakeStream, fakeStream, b.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); } } /** * 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(); } } }