Home | History | Annotate | Download | only in project
      1 /*
      2  * Copyright (C) 2008 The Android Open Source Project
      3  *
      4  * Licensed under the Eclipse Public License, Version 1.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.eclipse.org/org/documents/epl-v10.php
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package com.android.ide.eclipse.adt.internal.project;
     18 
     19 import com.android.ide.eclipse.adt.AdtConstants;
     20 import com.android.ide.eclipse.adt.AdtPlugin;
     21 import com.android.ide.eclipse.adt.AndroidPrintStream;
     22 import com.android.ide.eclipse.adt.internal.build.BuildHelper;
     23 import com.android.ide.eclipse.adt.internal.build.DexException;
     24 import com.android.ide.eclipse.adt.internal.build.NativeLibInJarException;
     25 import com.android.ide.eclipse.adt.internal.build.ProguardExecException;
     26 import com.android.ide.eclipse.adt.internal.build.ProguardResultException;
     27 import com.android.ide.eclipse.adt.internal.sdk.ProjectState;
     28 import com.android.ide.eclipse.adt.internal.sdk.Sdk;
     29 import com.android.ide.eclipse.adt.io.IFileWrapper;
     30 import com.android.sdklib.SdkConstants;
     31 import com.android.sdklib.build.ApkCreationException;
     32 import com.android.sdklib.build.DuplicateFileException;
     33 import com.android.sdklib.internal.project.ProjectProperties;
     34 import com.android.sdklib.xml.AndroidManifest;
     35 
     36 import org.eclipse.core.resources.IFile;
     37 import org.eclipse.core.resources.IProject;
     38 import org.eclipse.core.resources.IResource;
     39 import org.eclipse.core.runtime.CoreException;
     40 import org.eclipse.core.runtime.IProgressMonitor;
     41 import org.eclipse.core.runtime.IStatus;
     42 import org.eclipse.core.runtime.Status;
     43 import org.eclipse.core.runtime.jobs.Job;
     44 import org.eclipse.jdt.core.IJavaProject;
     45 import org.eclipse.jdt.core.JavaCore;
     46 import org.eclipse.swt.SWT;
     47 import org.eclipse.swt.widgets.Display;
     48 import org.eclipse.swt.widgets.FileDialog;
     49 import org.eclipse.swt.widgets.Shell;
     50 
     51 import java.io.BufferedInputStream;
     52 import java.io.File;
     53 import java.io.FileInputStream;
     54 import java.io.FileOutputStream;
     55 import java.io.IOException;
     56 import java.io.OutputStream;
     57 import java.security.PrivateKey;
     58 import java.security.cert.X509Certificate;
     59 import java.util.List;
     60 import java.util.jar.JarEntry;
     61 import java.util.jar.JarOutputStream;
     62 
     63 /**
     64  * Export helper to export release version of APKs.
     65  */
     66 public final class ExportHelper {
     67 
     68     private final static String TEMP_PREFIX = "android_";  //$NON-NLS-1$
     69 
     70     /**
     71      * Exports a release version of the application created by the given project.
     72      * @param project the project to export
     73      * @param outputFile the file to write
     74      * @param key the key to used for signing. Can be null.
     75      * @param certificate the certificate used for signing. Can be null.
     76      * @param monitor
     77      */
     78     public static void exportReleaseApk(IProject project, File outputFile, PrivateKey key,
     79             X509Certificate certificate, IProgressMonitor monitor) throws CoreException {
     80 
     81         // the export, takes the output of the precompiler & Java builders so it's
     82         // important to call build in case the auto-build option of the workspace is disabled.
     83         // Also enable post compilation and dependency building
     84         ProjectHelper.build(project, monitor, true, true);
     85 
     86         // if either key or certificate is null, ensure the other is null.
     87         if (key == null) {
     88             certificate = null;
     89         } else if (certificate == null) {
     90             key = null;
     91         }
     92 
     93         try {
     94             // check if the manifest declares debuggable as true. While this is a release build,
     95             // debuggable in the manifest will override this and generate a debug build
     96             IResource manifestResource = project.findMember(SdkConstants.FN_ANDROID_MANIFEST_XML);
     97             if (manifestResource.getType() != IResource.FILE) {
     98                 throw new CoreException(new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID,
     99                         String.format("%1$s missing.", SdkConstants.FN_ANDROID_MANIFEST_XML)));
    100             }
    101 
    102             IFileWrapper manifestFile = new IFileWrapper((IFile) manifestResource);
    103             boolean debugMode = AndroidManifest.getDebuggable(manifestFile);
    104 
    105             AndroidPrintStream fakeStream = new AndroidPrintStream(null, null, new OutputStream() {
    106                 @Override
    107                 public void write(int b) throws IOException {
    108                     // do nothing
    109                 }
    110             });
    111 
    112             BuildHelper helper = new BuildHelper(project,
    113                     fakeStream, fakeStream,
    114                     debugMode, false /*verbose*/);
    115 
    116             // get the list of library projects
    117             ProjectState projectState = Sdk.getProjectState(project);
    118             List<IProject> libProjects = projectState.getFullLibraryProjects();
    119 
    120             // Step 1. Package the resources.
    121 
    122             // tmp file for the packaged resource file. To not disturb the incremental builders
    123             // output, all intermediary files are created in tmp files.
    124             File resourceFile = File.createTempFile(TEMP_PREFIX, AdtConstants.DOT_RES);
    125             resourceFile.deleteOnExit();
    126 
    127             // Make sure the PNG crunch cache is up to date
    128             helper.updateCrunchCache();
    129 
    130             // package the resources.
    131             helper.packageResources(
    132                     project.getFile(SdkConstants.FN_ANDROID_MANIFEST_XML),
    133                     libProjects,
    134                     null,   // res filter
    135                     0,      // versionCode
    136                     resourceFile.getParent(),
    137                     resourceFile.getName());
    138 
    139             // Step 2. Convert the byte code to Dalvik bytecode
    140 
    141             // tmp file for the packaged resource file.
    142             File dexFile = File.createTempFile(TEMP_PREFIX, AdtConstants.DOT_DEX);
    143             dexFile.deleteOnExit();
    144 
    145             ProjectState state = Sdk.getProjectState(project);
    146             String proguardConfig = state.getProperties().getProperty(
    147                     ProjectProperties.PROPERTY_PROGUARD_CONFIG);
    148 
    149             boolean runProguard = false;
    150             File proguardConfigFile = null;
    151             if (proguardConfig != null && proguardConfig.length() > 0) {
    152                 proguardConfigFile = new File(proguardConfig);
    153                 if (proguardConfigFile.isAbsolute() == false) {
    154                     proguardConfigFile = new File(project.getLocation().toFile(), proguardConfig);
    155                 }
    156                 runProguard = proguardConfigFile.isFile();
    157             }
    158 
    159             String[] dxInput;
    160 
    161             if (runProguard) {
    162                 // the output of the main project (and any java-only project dependency)
    163                 String[] projectOutputs = helper.getProjectJavaOutputs();
    164 
    165                 // create a jar from the output of these projects
    166                 File inputJar = File.createTempFile(TEMP_PREFIX, AdtConstants.DOT_JAR);
    167                 inputJar.deleteOnExit();
    168 
    169                 JarOutputStream jos = new JarOutputStream(new FileOutputStream(inputJar));
    170                 for (String po : projectOutputs) {
    171                     File root = new File(po);
    172                     if (root.exists()) {
    173                         addFileToJar(jos, root, root);
    174                     }
    175                 }
    176                 jos.close();
    177 
    178                 // get the other jar files
    179                 String[] jarFiles = helper.getCompiledCodePaths(false /*includeProjectOutputs*/,
    180                         null /*resourceMarker*/);
    181 
    182                 // destination file for proguard
    183                 File obfuscatedJar = File.createTempFile(TEMP_PREFIX, AdtConstants.DOT_JAR);
    184                 obfuscatedJar.deleteOnExit();
    185 
    186                 // run proguard
    187                 helper.runProguard(proguardConfigFile, inputJar, jarFiles, obfuscatedJar,
    188                         new File(project.getLocation().toFile(), SdkConstants.FD_PROGUARD));
    189 
    190                 // dx input is proguard's output
    191                 dxInput = new String[] { obfuscatedJar.getAbsolutePath() };
    192             } else {
    193                 // no proguard, simply get all the compiled code path: project output(s) +
    194                 // jar file(s)
    195                 dxInput = helper.getCompiledCodePaths(true /*includeProjectOutputs*/,
    196                         null /*resourceMarker*/);
    197             }
    198 
    199             IJavaProject javaProject = JavaCore.create(project);
    200             List<IProject> javaProjects = ProjectHelper.getReferencedProjects(project);
    201             List<IJavaProject> referencedJavaProjects = BuildHelper.getJavaProjects(
    202                     javaProjects);
    203 
    204             helper.executeDx(javaProject, dxInput, dexFile.getAbsolutePath());
    205 
    206             // Step 3. Final package
    207 
    208             helper.finalPackage(
    209                     resourceFile.getAbsolutePath(),
    210                     dexFile.getAbsolutePath(),
    211                     outputFile.getAbsolutePath(),
    212                     javaProject,
    213                     libProjects,
    214                     referencedJavaProjects,
    215                     key,
    216                     certificate,
    217                     null); //resourceMarker
    218 
    219             // success!
    220         } catch (CoreException e) {
    221             throw e;
    222         } catch (ProguardResultException e) {
    223             String msg = String.format("Proguard returned with error code %d. See console",
    224                     e.getErrorCode());
    225             AdtPlugin.printErrorToConsole(project, msg);
    226             AdtPlugin.printErrorToConsole(project, (Object[]) e.getOutput());
    227             throw new CoreException(new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID,
    228                     msg, e));
    229         } catch (ProguardExecException e) {
    230             String msg = String.format("Failed to run proguard: %s", e.getMessage());
    231             AdtPlugin.printErrorToConsole(project, msg);
    232             throw new CoreException(new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID,
    233                     msg, e));
    234         } catch (DuplicateFileException e) {
    235             String msg = String.format(
    236                     "Found duplicate file for APK: %1$s\nOrigin 1: %2$s\nOrigin 2: %3$s",
    237                     e.getArchivePath(), e.getFile1(), e.getFile2());
    238             AdtPlugin.printErrorToConsole(project, msg);
    239             throw new CoreException(new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID,
    240                     e.getMessage(), e));
    241         } catch (NativeLibInJarException e) {
    242             String msg = e.getMessage();
    243 
    244             AdtPlugin.printErrorToConsole(project, msg);
    245             AdtPlugin.printErrorToConsole(project, (Object[]) e.getAdditionalInfo());
    246             throw new CoreException(new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID,
    247                     e.getMessage(), e));
    248         } catch (DexException e) {
    249             throw new CoreException(new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID,
    250                     e.getMessage(), e));
    251         } catch (ApkCreationException e) {
    252             throw new CoreException(new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID,
    253                     e.getMessage(), e));
    254         } catch (Exception e) {
    255             throw new CoreException(new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID,
    256                     "Failed to export application", e));
    257         }
    258     }
    259 
    260     /**
    261      * Exports an unsigned release APK after prompting the user for a location.
    262      *
    263      * <strong>Must be called from the UI thread.</strong>
    264      *
    265      * @param project the project to export
    266      */
    267     public static void exportUnsignedReleaseApk(final IProject project) {
    268         Shell shell = Display.getCurrent().getActiveShell();
    269 
    270         // create a default file name for the apk.
    271         String fileName = project.getName() + AdtConstants.DOT_ANDROID_PACKAGE;
    272 
    273         // Pop up the file save window to get the file location
    274         FileDialog fileDialog = new FileDialog(shell, SWT.SAVE);
    275 
    276         fileDialog.setText("Export Project");
    277         fileDialog.setFileName(fileName);
    278 
    279         final String saveLocation = fileDialog.open();
    280         if (saveLocation != null) {
    281             new Job("Android Release Export") {
    282                 @Override
    283                 protected IStatus run(IProgressMonitor monitor) {
    284                     try {
    285                         exportReleaseApk(project,
    286                                 new File(saveLocation),
    287                                 null, //key
    288                                 null, //certificate
    289                                 monitor);
    290 
    291                         // this is unsigned export. Let's tell the developers to run zip align
    292                         AdtPlugin.displayWarning("Android IDE Plug-in", String.format(
    293                                 "An unsigned package of the application was saved at\n%1$s\n\n" +
    294                                 "Before publishing the application you will need to:\n" +
    295                                 "- Sign the application with your release key,\n" +
    296                                 "- run zipalign on the signed package. ZipAlign is located in <SDK>/tools/\n\n" +
    297                                 "Aligning applications allows Android to use application resources\n" +
    298                                 "more efficiently.", saveLocation));
    299 
    300                         return Status.OK_STATUS;
    301                     } catch (CoreException e) {
    302                         AdtPlugin.displayError("Android IDE Plug-in", String.format(
    303                                 "Error exporting application:\n\n%1$s", e.getMessage()));
    304                         return e.getStatus();
    305                     }
    306                 }
    307             }.schedule();
    308         }
    309     }
    310 
    311     /**
    312      * Adds a file to a jar file.
    313      * The <var>rootDirectory</var> dictates the path of the file inside the jar file. It must be
    314      * a parent of <var>file</var>.
    315      * @param jar the jar to add the file to
    316      * @param file the file to add
    317      * @param rootDirectory the rootDirectory.
    318      * @throws IOException
    319      */
    320     private static void addFileToJar(JarOutputStream jar, File file, File rootDirectory)
    321             throws IOException {
    322         if (file.isDirectory()) {
    323             for (File child: file.listFiles()) {
    324                 addFileToJar(jar, child, rootDirectory);
    325             }
    326 
    327         } else if (file.isFile()) {
    328             // check the extension
    329             String name = file.getName();
    330             if (name.toLowerCase().endsWith(AdtConstants.DOT_CLASS) == false) {
    331                 return;
    332             }
    333 
    334             String rootPath = rootDirectory.getAbsolutePath();
    335             String path = file.getAbsolutePath();
    336             path = path.substring(rootPath.length()).replace("\\", "/"); //$NON-NLS-1$ //$NON-NLS-2$
    337             if (path.charAt(0) == '/') {
    338                 path = path.substring(1);
    339             }
    340 
    341             JarEntry entry = new JarEntry(path);
    342             entry.setTime(file.lastModified());
    343             jar.putNextEntry(entry);
    344 
    345             // put the content of the file.
    346             byte[] buffer = new byte[1024];
    347             int count;
    348             BufferedInputStream bis = new BufferedInputStream(new FileInputStream(file));
    349             while ((count = bis.read(buffer)) != -1) {
    350                 jar.write(buffer, 0, count);
    351             }
    352             jar.closeEntry();
    353         }
    354     }
    355 }
    356