diff options
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.java | 448 |
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(); + } + } +} |