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