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 static com.android.sdklib.internal.project.ProjectProperties.PROPERTY_SDK;
     20 
     21 import com.android.ide.eclipse.adt.AdtConstants;
     22 import com.android.ide.eclipse.adt.AdtPlugin;
     23 import com.android.ide.eclipse.adt.AndroidPrintStream;
     24 import com.android.ide.eclipse.adt.internal.build.BuildHelper;
     25 import com.android.ide.eclipse.adt.internal.build.DexException;
     26 import com.android.ide.eclipse.adt.internal.build.NativeLibInJarException;
     27 import com.android.ide.eclipse.adt.internal.build.ProguardExecException;
     28 import com.android.ide.eclipse.adt.internal.build.ProguardResultException;
     29 import com.android.ide.eclipse.adt.internal.preferences.AdtPrefs;
     30 import com.android.ide.eclipse.adt.internal.sdk.ProjectState;
     31 import com.android.ide.eclipse.adt.internal.sdk.Sdk;
     32 import com.android.ide.eclipse.adt.io.IFileWrapper;
     33 import com.android.sdklib.SdkConstants;
     34 import com.android.sdklib.build.ApkCreationException;
     35 import com.android.sdklib.build.DuplicateFileException;
     36 import com.android.sdklib.internal.project.ProjectProperties;
     37 import com.android.sdklib.xml.AndroidManifest;
     38 
     39 import org.eclipse.core.resources.IFile;
     40 import org.eclipse.core.resources.IProject;
     41 import org.eclipse.core.resources.IResource;
     42 import org.eclipse.core.resources.IncrementalProjectBuilder;
     43 import org.eclipse.core.runtime.CoreException;
     44 import org.eclipse.core.runtime.IProgressMonitor;
     45 import org.eclipse.core.runtime.IStatus;
     46 import org.eclipse.core.runtime.Status;
     47 import org.eclipse.core.runtime.jobs.Job;
     48 import org.eclipse.jdt.core.IJavaProject;
     49 import org.eclipse.jdt.core.JavaCore;
     50 import org.eclipse.swt.SWT;
     51 import org.eclipse.swt.widgets.Display;
     52 import org.eclipse.swt.widgets.FileDialog;
     53 import org.eclipse.swt.widgets.Shell;
     54 
     55 import java.io.BufferedInputStream;
     56 import java.io.File;
     57 import java.io.FileInputStream;
     58 import java.io.FileOutputStream;
     59 import java.io.IOException;
     60 import java.io.OutputStream;
     61 import java.security.PrivateKey;
     62 import java.security.cert.X509Certificate;
     63 import java.util.ArrayList;
     64 import java.util.Collection;
     65 import java.util.Collections;
     66 import java.util.List;
     67 import java.util.jar.JarEntry;
     68 import java.util.jar.JarOutputStream;
     69 
     70 /**
     71  * Export helper to export release version of APKs.
     72  */
     73 public final class ExportHelper {
     74     private static final String HOME_PROPERTY = "user.home";                    //$NON-NLS-1$
     75     private static final String HOME_PROPERTY_REF = "${" + HOME_PROPERTY + '}'; //$NON-NLS-1$
     76     private static final String SDK_PROPERTY_REF = "${" + PROPERTY_SDK + '}';   //$NON-NLS-1$
     77     private final static String TEMP_PREFIX = "android_";                       //$NON-NLS-1$
     78 
     79     /**
     80      * Exports a release version of the application created by the given project.
     81      * @param project the project to export
     82      * @param outputFile the file to write
     83      * @param key the key to used for signing. Can be null.
     84      * @param certificate the certificate used for signing. Can be null.
     85      * @param monitor progress monitor
     86      * @throws CoreException if an error occurs
     87      */
     88     public static void exportReleaseApk(IProject project, File outputFile, PrivateKey key,
     89             X509Certificate certificate, IProgressMonitor monitor) throws CoreException {
     90 
     91         // the export, takes the output of the precompiler & Java builders so it's
     92         // important to call build in case the auto-build option of the workspace is disabled.
     93         // Also enable dependency building to make sure everything is up to date.
     94         // However do not package the APK since we're going to do it manually here, using a
     95         // different output location.
     96         ProjectHelper.compileInReleaseMode(project, monitor);
     97 
     98         // if either key or certificate is null, ensure the other is null.
     99         if (key == null) {
    100             certificate = null;
    101         } else if (certificate == null) {
    102             key = null;
    103         }
    104 
    105         try {
    106             // check if the manifest declares debuggable as true. While this is a release build,
    107             // debuggable in the manifest will override this and generate a debug build
    108             IResource manifestResource = project.findMember(SdkConstants.FN_ANDROID_MANIFEST_XML);
    109             if (manifestResource.getType() != IResource.FILE) {
    110                 throw new CoreException(new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID,
    111                         String.format("%1$s missing.", SdkConstants.FN_ANDROID_MANIFEST_XML)));
    112             }
    113 
    114             IFileWrapper manifestFile = new IFileWrapper((IFile) manifestResource);
    115             boolean debugMode = AndroidManifest.getDebuggable(manifestFile);
    116 
    117             AndroidPrintStream fakeStream = new AndroidPrintStream(null, null, new OutputStream() {
    118                 @Override
    119                 public void write(int b) throws IOException {
    120                     // do nothing
    121                 }
    122             });
    123 
    124             BuildHelper helper = new BuildHelper(project,
    125                     fakeStream, fakeStream,
    126                     debugMode, false /*verbose*/,
    127                     null /*resourceMarker*/);
    128 
    129             // get the list of library projects
    130             ProjectState projectState = Sdk.getProjectState(project);
    131             List<IProject> libProjects = projectState.getFullLibraryProjects();
    132 
    133             // Step 1. Package the resources.
    134 
    135             // tmp file for the packaged resource file. To not disturb the incremental builders
    136             // output, all intermediary files are created in tmp files.
    137             File resourceFile = File.createTempFile(TEMP_PREFIX, AdtConstants.DOT_RES);
    138             resourceFile.deleteOnExit();
    139 
    140             // Make sure the PNG crunch cache is up to date
    141             helper.updateCrunchCache();
    142 
    143             // package the resources.
    144             helper.packageResources(
    145                     project.getFile(SdkConstants.FN_ANDROID_MANIFEST_XML),
    146                     libProjects,
    147                     null,   // res filter
    148                     0,      // versionCode
    149                     resourceFile.getParent(),
    150                     resourceFile.getName());
    151 
    152             // Step 2. Convert the byte code to Dalvik bytecode
    153 
    154             // tmp file for the packaged resource file.
    155             File dexFile = File.createTempFile(TEMP_PREFIX, AdtConstants.DOT_DEX);
    156             dexFile.deleteOnExit();
    157 
    158             ProjectState state = Sdk.getProjectState(project);
    159             String proguardConfig = state.getProperties().getProperty(
    160                     ProjectProperties.PROPERTY_PROGUARD_CONFIG);
    161 
    162             boolean runProguard = false;
    163             List<File> proguardConfigFiles = null;
    164             if (proguardConfig != null && proguardConfig.length() > 0) {
    165                 // Be tolerant with respect to file and path separators just like
    166                 // Ant is. Allow "/" in the property file to mean whatever the file
    167                 // separator character is:
    168                 if (File.separatorChar != '/' && proguardConfig.indexOf('/') != -1) {
    169                     proguardConfig = proguardConfig.replace('/', File.separatorChar);
    170                 }
    171                 // Also split path: no need to convert to File.pathSeparator because we'll
    172                 // be splitting the path ourselves right here, so just ensure that both
    173                 // ':' and ';' work:
    174                 if (proguardConfig.indexOf(';') != -1) {
    175                     proguardConfig = proguardConfig.replace(';', ':');
    176                 }
    177                 String[] paths = proguardConfig.split(":"); //$NON-NLS-1$
    178 
    179                 for (String path : paths) {
    180                     if (path.startsWith(SDK_PROPERTY_REF)) {
    181                         path = AdtPrefs.getPrefs().getOsSdkFolder() +
    182                                 path.substring(SDK_PROPERTY_REF.length());
    183                     } else if (path.startsWith(HOME_PROPERTY_REF)) {
    184                         path = System.getProperty(HOME_PROPERTY) +
    185                                 path.substring(HOME_PROPERTY_REF.length());
    186                     }
    187                     File proguardConfigFile = new File(path);
    188                     if (proguardConfigFile.isAbsolute() == false) {
    189                         proguardConfigFile = new File(project.getLocation().toFile(), path);
    190                     }
    191                     if (proguardConfigFile.isFile()) {
    192                         if (proguardConfigFiles == null) {
    193                             proguardConfigFiles = new ArrayList<File>();
    194                         }
    195                         proguardConfigFiles.add(proguardConfigFile);
    196                         runProguard = true;
    197                     } else {
    198                         throw new CoreException(new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID,
    199                                 "Invalid proguard configuration file path " + proguardConfigFile
    200                                 + " does not exist or is not a regular file", null));
    201                     }
    202                 }
    203             }
    204 
    205             Collection<String> dxInput;
    206 
    207             if (runProguard) {
    208                 // get all the compiled code paths. This will contain both project output
    209                 // folder and jar files.
    210                 Collection<String> paths = helper.getCompiledCodePaths();
    211 
    212                 // create a jar file containing all the project output (as proguard cannot
    213                 // process folders of .class files).
    214                 File inputJar = File.createTempFile(TEMP_PREFIX, AdtConstants.DOT_JAR);
    215                 inputJar.deleteOnExit();
    216                 JarOutputStream jos = new JarOutputStream(new FileOutputStream(inputJar));
    217 
    218                 // a list of the other paths (jar files.)
    219                 List<String> jars = new ArrayList<String>();
    220 
    221                 for (String path : paths) {
    222                     File root = new File(path);
    223                     if (root.isDirectory()) {
    224                         addFileToJar(jos, root, root);
    225                     } else if (root.isFile()) {
    226                         jars.add(path);
    227                     }
    228                 }
    229                 jos.close();
    230 
    231                 // destination file for proguard
    232                 File obfuscatedJar = File.createTempFile(TEMP_PREFIX, AdtConstants.DOT_JAR);
    233                 obfuscatedJar.deleteOnExit();
    234 
    235                 // run proguard
    236                 helper.runProguard(proguardConfigFiles, inputJar, jars, obfuscatedJar,
    237                         new File(project.getLocation().toFile(), SdkConstants.FD_PROGUARD));
    238 
    239                 helper.setProguardOutput(obfuscatedJar.getAbsolutePath());
    240 
    241                 // dx input is proguard's output
    242                 dxInput = Collections.singletonList(obfuscatedJar.getAbsolutePath());
    243             } else {
    244                 // no proguard, simply get all the compiled code path: project output(s) +
    245                 // jar file(s)
    246                 dxInput = helper.getCompiledCodePaths();
    247             }
    248 
    249             IJavaProject javaProject = JavaCore.create(project);
    250 
    251             helper.executeDx(javaProject, dxInput, dexFile.getAbsolutePath());
    252 
    253             // Step 3. Final package
    254 
    255             helper.finalPackage(
    256                     resourceFile.getAbsolutePath(),
    257                     dexFile.getAbsolutePath(),
    258                     outputFile.getAbsolutePath(),
    259                     libProjects,
    260                     key,
    261                     certificate,
    262                     null); //resourceMarker
    263 
    264             // success!
    265         } catch (CoreException e) {
    266             throw e;
    267         } catch (ProguardResultException e) {
    268             String msg = String.format("Proguard returned with error code %d. See console",
    269                     e.getErrorCode());
    270             AdtPlugin.printErrorToConsole(project, msg);
    271             AdtPlugin.printErrorToConsole(project, (Object[]) e.getOutput());
    272             throw new CoreException(new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID,
    273                     msg, e));
    274         } catch (ProguardExecException e) {
    275             String msg = String.format("Failed to run proguard: %s", e.getMessage());
    276             AdtPlugin.printErrorToConsole(project, msg);
    277             throw new CoreException(new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID,
    278                     msg, e));
    279         } catch (DuplicateFileException e) {
    280             String msg = String.format(
    281                     "Found duplicate file for APK: %1$s\nOrigin 1: %2$s\nOrigin 2: %3$s",
    282                     e.getArchivePath(), e.getFile1(), e.getFile2());
    283             AdtPlugin.printErrorToConsole(project, msg);
    284             throw new CoreException(new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID,
    285                     e.getMessage(), e));
    286         } catch (NativeLibInJarException e) {
    287             String msg = e.getMessage();
    288 
    289             AdtPlugin.printErrorToConsole(project, msg);
    290             AdtPlugin.printErrorToConsole(project, (Object[]) e.getAdditionalInfo());
    291             throw new CoreException(new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID,
    292                     e.getMessage(), e));
    293         } catch (DexException e) {
    294             throw new CoreException(new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID,
    295                     e.getMessage(), e));
    296         } catch (ApkCreationException e) {
    297             throw new CoreException(new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID,
    298                     e.getMessage(), e));
    299         } catch (Exception e) {
    300             throw new CoreException(new Status(IStatus.ERROR, AdtPlugin.PLUGIN_ID,
    301                     "Failed to export application", e));
    302         } finally {
    303             // move back to a debug build.
    304             // By using a normal build, we'll simply rebuild the debug version, and let the
    305             // builder decide whether to build the full package or not.
    306             ProjectHelper.buildWithDeps(project, IncrementalProjectBuilder.FULL_BUILD, monitor);
    307             project.refreshLocal(IResource.DEPTH_INFINITE, monitor);
    308         }
    309     }
    310 
    311     /**
    312      * Exports an unsigned release APK after prompting the user for a location.
    313      *
    314      * <strong>Must be called from the UI thread.</strong>
    315      *
    316      * @param project the project to export
    317      */
    318     public static void exportUnsignedReleaseApk(final IProject project) {
    319         Shell shell = Display.getCurrent().getActiveShell();
    320 
    321         // create a default file name for the apk.
    322         String fileName = project.getName() + AdtConstants.DOT_ANDROID_PACKAGE;
    323 
    324         // Pop up the file save window to get the file location
    325         FileDialog fileDialog = new FileDialog(shell, SWT.SAVE);
    326 
    327         fileDialog.setText("Export Project");
    328         fileDialog.setFileName(fileName);
    329 
    330         final String saveLocation = fileDialog.open();
    331         if (saveLocation != null) {
    332             new Job("Android Release Export") {
    333                 @Override
    334                 protected IStatus run(IProgressMonitor monitor) {
    335                     try {
    336                         exportReleaseApk(project,
    337                                 new File(saveLocation),
    338                                 null, //key
    339                                 null, //certificate
    340                                 monitor);
    341 
    342                         // this is unsigned export. Let's tell the developers to run zip align
    343                         AdtPlugin.displayWarning("Android IDE Plug-in", String.format(
    344                                 "An unsigned package of the application was saved at\n%1$s\n\n" +
    345                                 "Before publishing the application you will need to:\n" +
    346                                 "- Sign the application with your release key,\n" +
    347                                 "- run zipalign on the signed package. ZipAlign is located in <SDK>/tools/\n\n" +
    348                                 "Aligning applications allows Android to use application resources\n" +
    349                                 "more efficiently.", saveLocation));
    350 
    351                         return Status.OK_STATUS;
    352                     } catch (CoreException e) {
    353                         AdtPlugin.displayError("Android IDE Plug-in", String.format(
    354                                 "Error exporting application:\n\n%1$s", e.getMessage()));
    355                         return e.getStatus();
    356                     }
    357                 }
    358             }.schedule();
    359         }
    360     }
    361 
    362     /**
    363      * Adds a file to a jar file.
    364      * The <var>rootDirectory</var> dictates the path of the file inside the jar file. It must be
    365      * a parent of <var>file</var>.
    366      * @param jar the jar to add the file to
    367      * @param file the file to add
    368      * @param rootDirectory the rootDirectory.
    369      * @throws IOException
    370      */
    371     private static void addFileToJar(JarOutputStream jar, File file, File rootDirectory)
    372             throws IOException {
    373         if (file.isDirectory()) {
    374             if (file.getName().equals("META-INF") == false) {
    375                 for (File child: file.listFiles()) {
    376                     addFileToJar(jar, child, rootDirectory);
    377                 }
    378             }
    379         } else if (file.isFile()) {
    380             String rootPath = rootDirectory.getAbsolutePath();
    381             String path = file.getAbsolutePath();
    382             path = path.substring(rootPath.length()).replace("\\", "/"); //$NON-NLS-1$ //$NON-NLS-2$
    383             if (path.charAt(0) == '/') {
    384                 path = path.substring(1);
    385             }
    386 
    387             JarEntry entry = new JarEntry(path);
    388             entry.setTime(file.lastModified());
    389             jar.putNextEntry(entry);
    390 
    391             // put the content of the file.
    392             byte[] buffer = new byte[1024];
    393             int count;
    394             BufferedInputStream bis = null;
    395             try {
    396                 bis = new BufferedInputStream(new FileInputStream(file));
    397                 while ((count = bis.read(buffer)) != -1) {
    398                     jar.write(buffer, 0, count);
    399                 }
    400             } finally {
    401                 if (bis != null) {
    402                     try {
    403                         bis.close();
    404                     } catch (IOException ignore) {
    405                     }
    406                 }
    407             }
    408             jar.closeEntry();
    409         }
    410     }
    411 }
    412