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