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