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.wizards.export; 18 19 import com.android.annotations.Nullable; 20 import com.android.ide.eclipse.adt.AdtPlugin; 21 import com.android.ide.eclipse.adt.internal.sdk.ProjectState; 22 import com.android.ide.eclipse.adt.internal.sdk.Sdk; 23 import com.android.ide.eclipse.adt.internal.utils.FingerprintUtils; 24 import com.android.ide.eclipse.adt.internal.preferences.AdtPrefs.BuildVerbosity; 25 import com.android.ide.eclipse.adt.internal.project.ExportHelper; 26 import com.android.ide.eclipse.adt.internal.project.ProjectHelper; 27 import com.android.sdklib.BuildToolInfo; 28 import com.android.sdklib.BuildToolInfo.PathId; 29 import com.android.sdklib.internal.build.DebugKeyProvider.IKeyGenOutput; 30 import com.android.sdklib.internal.build.KeystoreHelper; 31 import com.android.utils.GrabProcessOutput; 32 import com.android.utils.GrabProcessOutput.IProcessOutput; 33 import com.android.utils.GrabProcessOutput.Wait; 34 35 import org.eclipse.core.resources.IProject; 36 import org.eclipse.core.resources.IResource; 37 import org.eclipse.core.runtime.IAdaptable; 38 import org.eclipse.core.runtime.IProgressMonitor; 39 import org.eclipse.jface.operation.IRunnableWithProgress; 40 import org.eclipse.jface.resource.ImageDescriptor; 41 import org.eclipse.jface.viewers.IStructuredSelection; 42 import org.eclipse.jface.wizard.Wizard; 43 import org.eclipse.jface.wizard.WizardPage; 44 import org.eclipse.swt.events.VerifyEvent; 45 import org.eclipse.swt.events.VerifyListener; 46 import org.eclipse.swt.widgets.Text; 47 import org.eclipse.ui.IExportWizard; 48 import org.eclipse.ui.IWorkbench; 49 import org.eclipse.ui.PlatformUI; 50 51 import java.io.ByteArrayOutputStream; 52 import java.io.File; 53 import java.io.FileInputStream; 54 import java.io.IOException; 55 import java.io.PrintStream; 56 import java.lang.reflect.InvocationTargetException; 57 import java.security.KeyStore; 58 import java.security.KeyStore.PrivateKeyEntry; 59 import java.security.PrivateKey; 60 import java.security.cert.X509Certificate; 61 import java.util.ArrayList; 62 import java.util.List; 63 64 /** 65 * Export wizard to export an apk signed with a release key/certificate. 66 */ 67 public final class ExportWizard extends Wizard implements IExportWizard { 68 69 private static final String PROJECT_LOGO_LARGE = "icons/android-64.png"; //$NON-NLS-1$ 70 71 private static final String PAGE_PROJECT_CHECK = "Page_ProjectCheck"; //$NON-NLS-1$ 72 private static final String PAGE_KEYSTORE_SELECTION = "Page_KeystoreSelection"; //$NON-NLS-1$ 73 private static final String PAGE_KEY_CREATION = "Page_KeyCreation"; //$NON-NLS-1$ 74 private static final String PAGE_KEY_SELECTION = "Page_KeySelection"; //$NON-NLS-1$ 75 private static final String PAGE_KEY_CHECK = "Page_KeyCheck"; //$NON-NLS-1$ 76 77 static final String PROPERTY_KEYSTORE = "keystore"; //$NON-NLS-1$ 78 static final String PROPERTY_ALIAS = "alias"; //$NON-NLS-1$ 79 static final String PROPERTY_DESTINATION = "destination"; //$NON-NLS-1$ 80 81 static final int APK_FILE_SOURCE = 0; 82 static final int APK_FILE_DEST = 1; 83 static final int APK_COUNT = 2; 84 85 /** 86 * Base page class for the ExportWizard page. This class add the {@link #onShow()} callback. 87 */ 88 static abstract class ExportWizardPage extends WizardPage { 89 90 /** bit mask constant for project data change event */ 91 protected static final int DATA_PROJECT = 0x001; 92 /** bit mask constant for keystore data change event */ 93 protected static final int DATA_KEYSTORE = 0x002; 94 /** bit mask constant for key data change event */ 95 protected static final int DATA_KEY = 0x004; 96 97 protected static final VerifyListener sPasswordVerifier = new VerifyListener() { 98 @Override 99 public void verifyText(VerifyEvent e) { 100 // verify the characters are valid for password. 101 int len = e.text.length(); 102 103 // first limit to 127 characters max 104 if (len + ((Text)e.getSource()).getText().length() > 127) { 105 e.doit = false; 106 return; 107 } 108 109 // now only take non control characters 110 for (int i = 0 ; i < len ; i++) { 111 if (e.text.charAt(i) < 32) { 112 e.doit = false; 113 return; 114 } 115 } 116 } 117 }; 118 119 /** 120 * Bit mask indicating what changed while the page was hidden. 121 * @see #DATA_PROJECT 122 * @see #DATA_KEYSTORE 123 * @see #DATA_KEY 124 */ 125 protected int mProjectDataChanged = 0; 126 ExportWizardPage(String name)127 ExportWizardPage(String name) { 128 super(name); 129 } 130 onShow()131 abstract void onShow(); 132 133 @Override setVisible(boolean visible)134 public void setVisible(boolean visible) { 135 super.setVisible(visible); 136 if (visible) { 137 onShow(); 138 mProjectDataChanged = 0; 139 } 140 } 141 projectDataChanged(int changeMask)142 final void projectDataChanged(int changeMask) { 143 mProjectDataChanged |= changeMask; 144 } 145 146 /** 147 * Calls {@link #setErrorMessage(String)} and {@link #setPageComplete(boolean)} based on a 148 * {@link Throwable} object. 149 */ onException(Throwable t)150 protected void onException(Throwable t) { 151 String message = getExceptionMessage(t); 152 153 setErrorMessage(message); 154 setPageComplete(false); 155 } 156 } 157 158 private ExportWizardPage mPages[] = new ExportWizardPage[5]; 159 160 private IProject mProject; 161 162 private String mKeystore; 163 private String mKeystorePassword; 164 private boolean mKeystoreCreationMode; 165 166 private String mKeyAlias; 167 private String mKeyPassword; 168 private int mValidity; 169 private String mDName; 170 171 private PrivateKey mPrivateKey; 172 private X509Certificate mCertificate; 173 174 private File mDestinationFile; 175 176 private ExportWizardPage mKeystoreSelectionPage; 177 private ExportWizardPage mKeyCreationPage; 178 private ExportWizardPage mKeySelectionPage; 179 private ExportWizardPage mKeyCheckPage; 180 181 private boolean mKeyCreationMode; 182 183 private List<String> mExistingAliases; 184 ExportWizard()185 public ExportWizard() { 186 setHelpAvailable(false); // TODO have help 187 setWindowTitle("Export Android Application"); 188 setImageDescriptor(); 189 } 190 191 @Override addPages()192 public void addPages() { 193 addPage(mPages[0] = new ProjectCheckPage(this, PAGE_PROJECT_CHECK)); 194 addPage(mKeystoreSelectionPage = mPages[1] = new KeystoreSelectionPage(this, 195 PAGE_KEYSTORE_SELECTION)); 196 addPage(mKeyCreationPage = mPages[2] = new KeyCreationPage(this, PAGE_KEY_CREATION)); 197 addPage(mKeySelectionPage = mPages[3] = new KeySelectionPage(this, PAGE_KEY_SELECTION)); 198 addPage(mKeyCheckPage = mPages[4] = new KeyCheckPage(this, PAGE_KEY_CHECK)); 199 } 200 201 @Override performFinish()202 public boolean performFinish() { 203 // save the properties 204 ProjectHelper.saveStringProperty(mProject, PROPERTY_KEYSTORE, mKeystore); 205 ProjectHelper.saveStringProperty(mProject, PROPERTY_ALIAS, mKeyAlias); 206 ProjectHelper.saveStringProperty(mProject, PROPERTY_DESTINATION, 207 mDestinationFile.getAbsolutePath()); 208 209 // run the export in an UI runnable. 210 IWorkbench workbench = PlatformUI.getWorkbench(); 211 final boolean[] result = new boolean[1]; 212 try { 213 workbench.getProgressService().busyCursorWhile(new IRunnableWithProgress() { 214 /** 215 * Run the export. 216 * @throws InvocationTargetException 217 * @throws InterruptedException 218 */ 219 @Override 220 public void run(IProgressMonitor monitor) throws InvocationTargetException, 221 InterruptedException { 222 try { 223 result[0] = doExport(monitor); 224 } finally { 225 monitor.done(); 226 } 227 } 228 }); 229 } catch (InvocationTargetException e) { 230 return false; 231 } catch (InterruptedException e) { 232 return false; 233 } 234 235 return result[0]; 236 } 237 doExport(IProgressMonitor monitor)238 private boolean doExport(IProgressMonitor monitor) { 239 try { 240 // if needed, create the keystore and/or key. 241 if (mKeystoreCreationMode || mKeyCreationMode) { 242 final ArrayList<String> output = new ArrayList<String>(); 243 boolean createdStore = KeystoreHelper.createNewStore( 244 mKeystore, 245 null /*storeType*/, 246 mKeystorePassword, 247 mKeyAlias, 248 mKeyPassword, 249 mDName, 250 mValidity, 251 new IKeyGenOutput() { 252 @Override 253 public void err(String message) { 254 output.add(message); 255 } 256 @Override 257 public void out(String message) { 258 output.add(message); 259 } 260 }); 261 262 if (createdStore == false) { 263 // keystore creation error! 264 displayError(output.toArray(new String[output.size()])); 265 return false; 266 } 267 268 // keystore is created, now load the private key and certificate. 269 KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType()); 270 FileInputStream fis = new FileInputStream(mKeystore); 271 keyStore.load(fis, mKeystorePassword.toCharArray()); 272 fis.close(); 273 PrivateKeyEntry entry = (KeyStore.PrivateKeyEntry)keyStore.getEntry( 274 mKeyAlias, new KeyStore.PasswordProtection(mKeyPassword.toCharArray())); 275 276 if (entry != null) { 277 mPrivateKey = entry.getPrivateKey(); 278 mCertificate = (X509Certificate)entry.getCertificate(); 279 280 AdtPlugin.printToConsole(mProject, 281 String.format("New keystore %s has been created.", 282 mDestinationFile.getAbsolutePath()), 283 "Certificate fingerprints:", 284 String.format(" MD5 : %s", getCertMd5Fingerprint()), 285 String.format(" SHA1: %s", getCertSha1Fingerprint())); 286 287 } else { 288 // this really shouldn't happen since we now let the user choose the key 289 // from a list read from the store. 290 displayError("Could not find key"); 291 return false; 292 } 293 } 294 295 // check the private key/certificate again since it may have been created just above. 296 if (mPrivateKey != null && mCertificate != null) { 297 // check whether we can run zipalign. 298 boolean runZipAlign = false; 299 300 ProjectState projectState = Sdk.getProjectState(mProject); 301 BuildToolInfo buildToolInfo = ExportHelper.getBuildTools(projectState); 302 303 String zipAlignPath = buildToolInfo.getPath(PathId.ZIP_ALIGN); 304 runZipAlign = zipAlignPath != null && new File(zipAlignPath).isFile(); 305 306 File apkExportFile = mDestinationFile; 307 if (runZipAlign) { 308 // create a temp file for the original export. 309 apkExportFile = File.createTempFile("androidExport_", ".apk"); 310 } 311 312 // export the signed apk. 313 ExportHelper.exportReleaseApk(mProject, apkExportFile, 314 mPrivateKey, mCertificate, monitor); 315 316 // align if we can 317 if (runZipAlign) { 318 String message = zipAlign(zipAlignPath, apkExportFile, mDestinationFile); 319 if (message != null) { 320 displayError(message); 321 return false; 322 } 323 } else { 324 AdtPlugin.displayWarning("Export Wizard", 325 "The zipalign tool was not found in the SDK.\n\n" + 326 "Please update to the latest SDK and re-export your application\n" + 327 "or run zipalign manually.\n\n" + 328 "Aligning applications allows Android to use application resources\n" + 329 "more efficiently."); 330 } 331 332 return true; 333 } 334 } catch (Throwable t) { 335 displayError(t); 336 } 337 338 return false; 339 } 340 341 @Override canFinish()342 public boolean canFinish() { 343 // check if we have the apk to resign, the destination location, and either 344 // a private key/certificate or the creation mode. In creation mode, unless 345 // all the key/keystore info is valid, the user cannot reach the last page, so there's 346 // no need to check them again here. 347 return ((mPrivateKey != null && mCertificate != null) 348 || mKeystoreCreationMode || mKeyCreationMode) && 349 mDestinationFile != null; 350 } 351 352 /* 353 * (non-Javadoc) 354 * @see org.eclipse.ui.IWorkbenchWizard#init(org.eclipse.ui.IWorkbench, 355 * org.eclipse.jface.viewers.IStructuredSelection) 356 */ 357 @Override init(IWorkbench workbench, IStructuredSelection selection)358 public void init(IWorkbench workbench, IStructuredSelection selection) { 359 // get the project from the selection 360 Object selected = selection.getFirstElement(); 361 362 if (selected instanceof IProject) { 363 mProject = (IProject)selected; 364 } else if (selected instanceof IAdaptable) { 365 IResource r = (IResource)((IAdaptable)selected).getAdapter(IResource.class); 366 if (r != null) { 367 mProject = r.getProject(); 368 } 369 } 370 } 371 getKeystoreSelectionPage()372 ExportWizardPage getKeystoreSelectionPage() { 373 return mKeystoreSelectionPage; 374 } 375 getKeyCreationPage()376 ExportWizardPage getKeyCreationPage() { 377 return mKeyCreationPage; 378 } 379 getKeySelectionPage()380 ExportWizardPage getKeySelectionPage() { 381 return mKeySelectionPage; 382 } 383 getKeyCheckPage()384 ExportWizardPage getKeyCheckPage() { 385 return mKeyCheckPage; 386 } 387 388 /** 389 * Returns an image descriptor for the wizard logo. 390 */ setImageDescriptor()391 private void setImageDescriptor() { 392 ImageDescriptor desc = AdtPlugin.getImageDescriptor(PROJECT_LOGO_LARGE); 393 setDefaultPageImageDescriptor(desc); 394 } 395 getProject()396 IProject getProject() { 397 return mProject; 398 } 399 setProject(IProject project)400 void setProject(IProject project) { 401 mProject = project; 402 403 updatePageOnChange(ExportWizardPage.DATA_PROJECT); 404 } 405 setKeystore(String path)406 void setKeystore(String path) { 407 mKeystore = path; 408 mPrivateKey = null; 409 mCertificate = null; 410 411 updatePageOnChange(ExportWizardPage.DATA_KEYSTORE); 412 } 413 getKeystore()414 String getKeystore() { 415 return mKeystore; 416 } 417 setKeystoreCreationMode(boolean createStore)418 void setKeystoreCreationMode(boolean createStore) { 419 mKeystoreCreationMode = createStore; 420 updatePageOnChange(ExportWizardPage.DATA_KEYSTORE); 421 } 422 getKeystoreCreationMode()423 boolean getKeystoreCreationMode() { 424 return mKeystoreCreationMode; 425 } 426 427 setKeystorePassword(String password)428 void setKeystorePassword(String password) { 429 mKeystorePassword = password; 430 mPrivateKey = null; 431 mCertificate = null; 432 433 updatePageOnChange(ExportWizardPage.DATA_KEYSTORE); 434 } 435 getKeystorePassword()436 String getKeystorePassword() { 437 return mKeystorePassword; 438 } 439 setKeyCreationMode(boolean createKey)440 void setKeyCreationMode(boolean createKey) { 441 mKeyCreationMode = createKey; 442 updatePageOnChange(ExportWizardPage.DATA_KEY); 443 } 444 getKeyCreationMode()445 boolean getKeyCreationMode() { 446 return mKeyCreationMode; 447 } 448 setExistingAliases(List<String> aliases)449 void setExistingAliases(List<String> aliases) { 450 mExistingAliases = aliases; 451 } 452 getExistingAliases()453 List<String> getExistingAliases() { 454 return mExistingAliases; 455 } 456 setKeyAlias(String name)457 void setKeyAlias(String name) { 458 mKeyAlias = name; 459 mPrivateKey = null; 460 mCertificate = null; 461 462 updatePageOnChange(ExportWizardPage.DATA_KEY); 463 } 464 getKeyAlias()465 String getKeyAlias() { 466 return mKeyAlias; 467 } 468 setKeyPassword(String password)469 void setKeyPassword(String password) { 470 mKeyPassword = password; 471 mPrivateKey = null; 472 mCertificate = null; 473 474 updatePageOnChange(ExportWizardPage.DATA_KEY); 475 } 476 getKeyPassword()477 String getKeyPassword() { 478 return mKeyPassword; 479 } 480 setValidity(int validity)481 void setValidity(int validity) { 482 mValidity = validity; 483 updatePageOnChange(ExportWizardPage.DATA_KEY); 484 } 485 getValidity()486 int getValidity() { 487 return mValidity; 488 } 489 setDName(String dName)490 void setDName(String dName) { 491 mDName = dName; 492 updatePageOnChange(ExportWizardPage.DATA_KEY); 493 } 494 getDName()495 String getDName() { 496 return mDName; 497 } 498 getCertSha1Fingerprint()499 String getCertSha1Fingerprint() { 500 return FingerprintUtils.getFingerprint(mCertificate, "SHA1"); 501 } 502 getCertMd5Fingerprint()503 String getCertMd5Fingerprint() { 504 return FingerprintUtils.getFingerprint(mCertificate, "MD5"); 505 } 506 setSigningInfo(PrivateKey privateKey, X509Certificate certificate)507 void setSigningInfo(PrivateKey privateKey, X509Certificate certificate) { 508 mPrivateKey = privateKey; 509 mCertificate = certificate; 510 } 511 setDestination(File destinationFile)512 void setDestination(File destinationFile) { 513 mDestinationFile = destinationFile; 514 } 515 resetDestination()516 void resetDestination() { 517 mDestinationFile = null; 518 } 519 updatePageOnChange(int changeMask)520 void updatePageOnChange(int changeMask) { 521 for (ExportWizardPage page : mPages) { 522 page.projectDataChanged(changeMask); 523 } 524 } 525 displayError(String... messages)526 private void displayError(String... messages) { 527 String message = null; 528 if (messages.length == 1) { 529 message = messages[0]; 530 } else { 531 StringBuilder sb = new StringBuilder(messages[0]); 532 for (int i = 1; i < messages.length; i++) { 533 sb.append('\n'); 534 sb.append(messages[i]); 535 } 536 537 message = sb.toString(); 538 } 539 540 AdtPlugin.displayError("Export Wizard", message); 541 } 542 displayError(Throwable t)543 private void displayError(Throwable t) { 544 String message = getExceptionMessage(t); 545 displayError(message); 546 547 AdtPlugin.log(t, "Export Wizard Error"); 548 } 549 550 /** 551 * Executes zipalign 552 * @param zipAlignPath location of the zipalign too 553 * @param source file to zipalign 554 * @param destination where to write the resulting file 555 * @return null if success, the error otherwise 556 * @throws IOException 557 */ zipAlign(String zipAlignPath, File source, File destination)558 private String zipAlign(String zipAlignPath, File source, File destination) throws IOException { 559 // command line: zipaling -f 4 tmp destination 560 String[] command = new String[5]; 561 command[0] = zipAlignPath; 562 command[1] = "-f"; //$NON-NLS-1$ 563 command[2] = "4"; //$NON-NLS-1$ 564 command[3] = source.getAbsolutePath(); 565 command[4] = destination.getAbsolutePath(); 566 567 Process process = Runtime.getRuntime().exec(command); 568 final ArrayList<String> output = new ArrayList<String>(); 569 try { 570 final IProject project = getProject(); 571 572 int status = GrabProcessOutput.grabProcessOutput( 573 process, 574 Wait.WAIT_FOR_READERS, 575 new IProcessOutput() { 576 @Override 577 public void out(@Nullable String line) { 578 if (line != null) { 579 AdtPlugin.printBuildToConsole(BuildVerbosity.VERBOSE, 580 project, line); 581 } 582 } 583 584 @Override 585 public void err(@Nullable String line) { 586 if (line != null) { 587 output.add(line); 588 } 589 } 590 }); 591 592 if (status != 0) { 593 // build a single message from the array list 594 StringBuilder sb = new StringBuilder("Error while running zipalign:"); 595 for (String msg : output) { 596 sb.append('\n'); 597 sb.append(msg); 598 } 599 600 return sb.toString(); 601 } 602 } catch (InterruptedException e) { 603 // ? 604 } 605 return null; 606 } 607 608 /** 609 * Returns the {@link Throwable#getMessage()}. If the {@link Throwable#getMessage()} returns 610 * <code>null</code>, the method is called again on the cause of the Throwable object. 611 * <p/>If no Throwable in the chain has a valid message, the canonical name of the first 612 * exception is returned. 613 */ getExceptionMessage(Throwable t)614 static String getExceptionMessage(Throwable t) { 615 String message = t.getMessage(); 616 if (message == null) { 617 // no error info? get the stack call to display it 618 // At least that'll give us a better bug report. 619 ByteArrayOutputStream baos = new ByteArrayOutputStream(); 620 t.printStackTrace(new PrintStream(baos)); 621 message = baos.toString(); 622 } 623 624 return message; 625 } 626 } 627