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.ide.eclipse.adt.AdtConstants; 20 import com.android.ide.eclipse.adt.AdtPlugin; 21 import com.android.ide.eclipse.adt.internal.project.BaseProjectHelper; 22 import com.android.ide.eclipse.adt.internal.project.ProjectHelper; 23 import com.android.jarutils.KeystoreHelper; 24 import com.android.jarutils.SignedJarBuilder; 25 import com.android.jarutils.DebugKeyProvider.IKeyGenOutput; 26 27 import org.eclipse.core.resources.IFolder; 28 import org.eclipse.core.resources.IProject; 29 import org.eclipse.core.resources.IResource; 30 import org.eclipse.core.resources.IncrementalProjectBuilder; 31 import org.eclipse.core.runtime.IAdaptable; 32 import org.eclipse.core.runtime.IProgressMonitor; 33 import org.eclipse.jface.operation.IRunnableWithProgress; 34 import org.eclipse.jface.resource.ImageDescriptor; 35 import org.eclipse.jface.viewers.IStructuredSelection; 36 import org.eclipse.jface.wizard.Wizard; 37 import org.eclipse.jface.wizard.WizardPage; 38 import org.eclipse.swt.events.VerifyEvent; 39 import org.eclipse.swt.events.VerifyListener; 40 import org.eclipse.swt.widgets.Text; 41 import org.eclipse.ui.IExportWizard; 42 import org.eclipse.ui.IWorkbench; 43 import org.eclipse.ui.PlatformUI; 44 45 import java.io.BufferedReader; 46 import java.io.ByteArrayOutputStream; 47 import java.io.File; 48 import java.io.FileInputStream; 49 import java.io.FileOutputStream; 50 import java.io.IOException; 51 import java.io.InputStreamReader; 52 import java.io.PrintStream; 53 import java.lang.reflect.InvocationTargetException; 54 import java.security.KeyStore; 55 import java.security.PrivateKey; 56 import java.security.KeyStore.PrivateKeyEntry; 57 import java.security.cert.X509Certificate; 58 import java.util.ArrayList; 59 import java.util.List; 60 import java.util.Map; 61 import java.util.Set; 62 import java.util.Map.Entry; 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_large.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 static final String PROPERTY_FILENAME = "baseFilename"; //$NON-NLS-1$ 81 82 static final int APK_FILE_SOURCE = 0; 83 static final int APK_FILE_DEST = 1; 84 static final int APK_COUNT = 2; 85 86 /** 87 * Base page class for the ExportWizard page. This class add the {@link #onShow()} callback. 88 */ 89 static abstract class ExportWizardPage extends WizardPage { 90 91 /** bit mask constant for project data change event */ 92 protected static final int DATA_PROJECT = 0x001; 93 /** bit mask constant for keystore data change event */ 94 protected static final int DATA_KEYSTORE = 0x002; 95 /** bit mask constant for key data change event */ 96 protected static final int DATA_KEY = 0x004; 97 98 protected static final VerifyListener sPasswordVerifier = new VerifyListener() { 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 mDestinationParentFolder; 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 185 private Map<String, String[]> mApkMap; 186 ExportWizard()187 public ExportWizard() { 188 setHelpAvailable(false); // TODO have help 189 setWindowTitle("Export Android Application"); 190 setImageDescriptor(); 191 } 192 193 @Override addPages()194 public void addPages() { 195 addPage(mPages[0] = new ProjectCheckPage(this, PAGE_PROJECT_CHECK)); 196 addPage(mKeystoreSelectionPage = mPages[1] = new KeystoreSelectionPage(this, 197 PAGE_KEYSTORE_SELECTION)); 198 addPage(mKeyCreationPage = mPages[2] = new KeyCreationPage(this, PAGE_KEY_CREATION)); 199 addPage(mKeySelectionPage = mPages[3] = new KeySelectionPage(this, PAGE_KEY_SELECTION)); 200 addPage(mKeyCheckPage = mPages[4] = new KeyCheckPage(this, PAGE_KEY_CHECK)); 201 } 202 203 @Override performFinish()204 public boolean performFinish() { 205 // save the properties 206 ProjectHelper.saveStringProperty(mProject, PROPERTY_KEYSTORE, mKeystore); 207 ProjectHelper.saveStringProperty(mProject, PROPERTY_ALIAS, mKeyAlias); 208 ProjectHelper.saveStringProperty(mProject, PROPERTY_DESTINATION, 209 mDestinationParentFolder.getAbsolutePath()); 210 ProjectHelper.saveStringProperty(mProject, PROPERTY_FILENAME, 211 mApkMap.get(null)[APK_FILE_DEST]); 212 213 // run the export in an UI runnable. 214 IWorkbench workbench = PlatformUI.getWorkbench(); 215 final boolean[] result = new boolean[1]; 216 try { 217 workbench.getProgressService().busyCursorWhile(new IRunnableWithProgress() { 218 /** 219 * Run the export. 220 * @throws InvocationTargetException 221 * @throws InterruptedException 222 */ 223 public void run(IProgressMonitor monitor) throws InvocationTargetException, 224 InterruptedException { 225 try { 226 result[0] = doExport(monitor); 227 } finally { 228 monitor.done(); 229 } 230 } 231 }); 232 } catch (InvocationTargetException e) { 233 return false; 234 } catch (InterruptedException e) { 235 return false; 236 } 237 238 return result[0]; 239 } 240 doExport(IProgressMonitor monitor)241 private boolean doExport(IProgressMonitor monitor) { 242 try { 243 // first we make sure the project is built 244 mProject.build(IncrementalProjectBuilder.INCREMENTAL_BUILD, monitor); 245 246 // if needed, create the keystore and/or key. 247 if (mKeystoreCreationMode || mKeyCreationMode) { 248 final ArrayList<String> output = new ArrayList<String>(); 249 boolean createdStore = KeystoreHelper.createNewStore( 250 mKeystore, 251 null /*storeType*/, 252 mKeystorePassword, 253 mKeyAlias, 254 mKeyPassword, 255 mDName, 256 mValidity, 257 new IKeyGenOutput() { 258 public void err(String message) { 259 output.add(message); 260 } 261 public void out(String message) { 262 output.add(message); 263 } 264 }); 265 266 if (createdStore == false) { 267 // keystore creation error! 268 displayError(output.toArray(new String[output.size()])); 269 return false; 270 } 271 272 // keystore is created, now load the private key and certificate. 273 KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType()); 274 FileInputStream fis = new FileInputStream(mKeystore); 275 keyStore.load(fis, mKeystorePassword.toCharArray()); 276 fis.close(); 277 PrivateKeyEntry entry = (KeyStore.PrivateKeyEntry)keyStore.getEntry( 278 mKeyAlias, new KeyStore.PasswordProtection(mKeyPassword.toCharArray())); 279 280 if (entry != null) { 281 mPrivateKey = entry.getPrivateKey(); 282 mCertificate = (X509Certificate)entry.getCertificate(); 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 // get the output folder of the project to export. 294 // this is where we'll find the built apks to resign and export. 295 IFolder outputIFolder = BaseProjectHelper.getOutputFolder(mProject); 296 if (outputIFolder == null) { 297 return false; 298 } 299 String outputOsPath = outputIFolder.getLocation().toOSString(); 300 301 // now generate the packages. 302 Set<Entry<String, String[]>> set = mApkMap.entrySet(); 303 304 boolean runZipAlign = false; 305 String path = AdtPlugin.getOsAbsoluteZipAlign(); 306 File zipalign = new File(path); 307 runZipAlign = zipalign.isFile(); 308 309 for (Entry<String, String[]> entry : set) { 310 String[] defaultApk = entry.getValue(); 311 String srcFilename = defaultApk[APK_FILE_SOURCE]; 312 String destFilename = defaultApk[APK_FILE_DEST]; 313 File destFile; 314 if (runZipAlign) { 315 destFile = File.createTempFile("android", ".apk"); 316 } else { 317 destFile = new File(mDestinationParentFolder, destFilename); 318 } 319 320 321 FileOutputStream fos = new FileOutputStream(destFile); 322 SignedJarBuilder builder = new SignedJarBuilder(fos, mPrivateKey, mCertificate); 323 324 // get the input file. 325 FileInputStream fis = new FileInputStream(new File(outputOsPath, srcFilename)); 326 327 // add the content of the source file to the output file, and sign it at 328 // the same time. 329 try { 330 builder.writeZip(fis, null /* filter */); 331 // close the builder: write the final signature files, 332 // and close the archive. 333 builder.close(); 334 335 // now zipalign the result 336 if (runZipAlign) { 337 File realDestFile = new File(mDestinationParentFolder, destFilename); 338 String message = zipAlign(path, destFile, realDestFile); 339 if (message != null) { 340 displayError(message); 341 return false; 342 } 343 } 344 } finally { 345 try { 346 fis.close(); 347 } finally { 348 fos.close(); 349 } 350 } 351 } 352 353 // export success. In case we didn't run ZipAlign we display a warning 354 if (runZipAlign == false) { 355 AdtPlugin.displayWarning("Export Wizard", 356 "The zipalign tool was not found in the SDK.\n\n" + 357 "Please update to the latest SDK and re-export your application\n" + 358 "or run zipalign manually.\n\n" + 359 "Aligning applications allows Android to use application resources\n" + 360 "more efficiently."); 361 } 362 363 return true; 364 } 365 } catch (Throwable t) { 366 displayError(t); 367 } 368 369 return false; 370 } 371 372 @Override canFinish()373 public boolean canFinish() { 374 // check if we have the apk to resign, the destination location, and either 375 // a private key/certificate or the creation mode. In creation mode, unless 376 // all the key/keystore info is valid, the user cannot reach the last page, so there's 377 // no need to check them again here. 378 return mApkMap != null && mApkMap.size() > 0 && 379 ((mPrivateKey != null && mCertificate != null) 380 || mKeystoreCreationMode || mKeyCreationMode) && 381 mDestinationParentFolder != null; 382 } 383 384 /* 385 * (non-Javadoc) 386 * @see org.eclipse.ui.IWorkbenchWizard#init(org.eclipse.ui.IWorkbench, org.eclipse.jface.viewers.IStructuredSelection) 387 */ init(IWorkbench workbench, IStructuredSelection selection)388 public void init(IWorkbench workbench, IStructuredSelection selection) { 389 // get the project from the selection 390 Object selected = selection.getFirstElement(); 391 392 if (selected instanceof IProject) { 393 mProject = (IProject)selected; 394 } else if (selected instanceof IAdaptable) { 395 IResource r = (IResource)((IAdaptable)selected).getAdapter(IResource.class); 396 if (r != null) { 397 mProject = r.getProject(); 398 } 399 } 400 } 401 getKeystoreSelectionPage()402 ExportWizardPage getKeystoreSelectionPage() { 403 return mKeystoreSelectionPage; 404 } 405 getKeyCreationPage()406 ExportWizardPage getKeyCreationPage() { 407 return mKeyCreationPage; 408 } 409 getKeySelectionPage()410 ExportWizardPage getKeySelectionPage() { 411 return mKeySelectionPage; 412 } 413 getKeyCheckPage()414 ExportWizardPage getKeyCheckPage() { 415 return mKeyCheckPage; 416 } 417 418 /** 419 * Returns an image descriptor for the wizard logo. 420 */ setImageDescriptor()421 private void setImageDescriptor() { 422 ImageDescriptor desc = AdtPlugin.getImageDescriptor(PROJECT_LOGO_LARGE); 423 setDefaultPageImageDescriptor(desc); 424 } 425 getProject()426 IProject getProject() { 427 return mProject; 428 } 429 setProject(IProject project)430 void setProject(IProject project) { 431 mProject = project; 432 433 updatePageOnChange(ExportWizardPage.DATA_PROJECT); 434 } 435 setKeystore(String path)436 void setKeystore(String path) { 437 mKeystore = path; 438 mPrivateKey = null; 439 mCertificate = null; 440 441 updatePageOnChange(ExportWizardPage.DATA_KEYSTORE); 442 } 443 getKeystore()444 String getKeystore() { 445 return mKeystore; 446 } 447 setKeystoreCreationMode(boolean createStore)448 void setKeystoreCreationMode(boolean createStore) { 449 mKeystoreCreationMode = createStore; 450 updatePageOnChange(ExportWizardPage.DATA_KEYSTORE); 451 } 452 getKeystoreCreationMode()453 boolean getKeystoreCreationMode() { 454 return mKeystoreCreationMode; 455 } 456 457 setKeystorePassword(String password)458 void setKeystorePassword(String password) { 459 mKeystorePassword = password; 460 mPrivateKey = null; 461 mCertificate = null; 462 463 updatePageOnChange(ExportWizardPage.DATA_KEYSTORE); 464 } 465 getKeystorePassword()466 String getKeystorePassword() { 467 return mKeystorePassword; 468 } 469 setKeyCreationMode(boolean createKey)470 void setKeyCreationMode(boolean createKey) { 471 mKeyCreationMode = createKey; 472 updatePageOnChange(ExportWizardPage.DATA_KEY); 473 } 474 getKeyCreationMode()475 boolean getKeyCreationMode() { 476 return mKeyCreationMode; 477 } 478 setExistingAliases(List<String> aliases)479 void setExistingAliases(List<String> aliases) { 480 mExistingAliases = aliases; 481 } 482 getExistingAliases()483 List<String> getExistingAliases() { 484 return mExistingAliases; 485 } 486 setKeyAlias(String name)487 void setKeyAlias(String name) { 488 mKeyAlias = name; 489 mPrivateKey = null; 490 mCertificate = null; 491 492 updatePageOnChange(ExportWizardPage.DATA_KEY); 493 } 494 getKeyAlias()495 String getKeyAlias() { 496 return mKeyAlias; 497 } 498 setKeyPassword(String password)499 void setKeyPassword(String password) { 500 mKeyPassword = password; 501 mPrivateKey = null; 502 mCertificate = null; 503 504 updatePageOnChange(ExportWizardPage.DATA_KEY); 505 } 506 getKeyPassword()507 String getKeyPassword() { 508 return mKeyPassword; 509 } 510 setValidity(int validity)511 void setValidity(int validity) { 512 mValidity = validity; 513 updatePageOnChange(ExportWizardPage.DATA_KEY); 514 } 515 getValidity()516 int getValidity() { 517 return mValidity; 518 } 519 setDName(String dName)520 void setDName(String dName) { 521 mDName = dName; 522 updatePageOnChange(ExportWizardPage.DATA_KEY); 523 } 524 getDName()525 String getDName() { 526 return mDName; 527 } 528 setSigningInfo(PrivateKey privateKey, X509Certificate certificate)529 void setSigningInfo(PrivateKey privateKey, X509Certificate certificate) { 530 mPrivateKey = privateKey; 531 mCertificate = certificate; 532 } 533 setDestination(File parentFolder, Map<String, String[]> apkMap)534 void setDestination(File parentFolder, Map<String, String[]> apkMap) { 535 mDestinationParentFolder = parentFolder; 536 mApkMap = apkMap; 537 } 538 resetDestination()539 void resetDestination() { 540 mDestinationParentFolder = null; 541 mApkMap = null; 542 } 543 updatePageOnChange(int changeMask)544 void updatePageOnChange(int changeMask) { 545 for (ExportWizardPage page : mPages) { 546 page.projectDataChanged(changeMask); 547 } 548 } 549 displayError(String... messages)550 private void displayError(String... messages) { 551 String message = null; 552 if (messages.length == 1) { 553 message = messages[0]; 554 } else { 555 StringBuilder sb = new StringBuilder(messages[0]); 556 for (int i = 1; i < messages.length; i++) { 557 sb.append('\n'); 558 sb.append(messages[i]); 559 } 560 561 message = sb.toString(); 562 } 563 564 AdtPlugin.displayError("Export Wizard", message); 565 } 566 displayError(Throwable t)567 private void displayError(Throwable t) { 568 String message = getExceptionMessage(t); 569 displayError(message); 570 571 AdtPlugin.log(t, "Export Wizard Error"); 572 } 573 574 /** 575 * Executes zipalign 576 * @param zipAlignPath location of the zipalign too 577 * @param source file to zipalign 578 * @param destination where to write the resulting file 579 * @return null if success, the error otherwise 580 * @throws IOException 581 */ zipAlign(String zipAlignPath, File source, File destination)582 private String zipAlign(String zipAlignPath, File source, File destination) throws IOException { 583 // command line: zipaling -f 4 tmp destination 584 String[] command = new String[5]; 585 command[0] = zipAlignPath; 586 command[1] = "-f"; //$NON-NLS-1$ 587 command[2] = "4"; //$NON-NLS-1$ 588 command[3] = source.getAbsolutePath(); 589 command[4] = destination.getAbsolutePath(); 590 591 Process process = Runtime.getRuntime().exec(command); 592 ArrayList<String> output = new ArrayList<String>(); 593 try { 594 if (grabProcessOutput(process, output) != 0) { 595 // build a single message from the array list 596 StringBuilder sb = new StringBuilder("Error while running zipalign:"); 597 for (String msg : output) { 598 sb.append('\n'); 599 sb.append(msg); 600 } 601 602 return sb.toString(); 603 } 604 } catch (InterruptedException e) { 605 // ? 606 } 607 return null; 608 } 609 610 /** 611 * Get the stderr output of a process and return when the process is done. 612 * @param process The process to get the ouput from 613 * @param results The array to store the stderr output 614 * @return the process return code. 615 * @throws InterruptedException 616 */ grabProcessOutput(final Process process, final ArrayList<String> results)617 private final int grabProcessOutput(final Process process, 618 final ArrayList<String> results) 619 throws InterruptedException { 620 // Due to the limited buffer size on windows for the standard io (stderr, stdout), we 621 // *need* to read both stdout and stderr all the time. If we don't and a process output 622 // a large amount, this could deadlock the process. 623 624 // read the lines as they come. if null is returned, it's 625 // because the process finished 626 new Thread("") { //$NON-NLS-1$ 627 @Override 628 public void run() { 629 // create a buffer to read the stderr output 630 InputStreamReader is = new InputStreamReader(process.getErrorStream()); 631 BufferedReader errReader = new BufferedReader(is); 632 633 try { 634 while (true) { 635 String line = errReader.readLine(); 636 if (line != null) { 637 results.add(line); 638 } else { 639 break; 640 } 641 } 642 } catch (IOException e) { 643 // do nothing. 644 } 645 } 646 }.start(); 647 648 new Thread("") { //$NON-NLS-1$ 649 @Override 650 public void run() { 651 InputStreamReader is = new InputStreamReader(process.getInputStream()); 652 BufferedReader outReader = new BufferedReader(is); 653 654 IProject project = getProject(); 655 656 try { 657 while (true) { 658 String line = outReader.readLine(); 659 if (line != null) { 660 AdtPlugin.printBuildToConsole(AdtConstants.BUILD_VERBOSE, 661 project, line); 662 } else { 663 break; 664 } 665 } 666 } catch (IOException e) { 667 // do nothing. 668 } 669 } 670 671 }.start(); 672 673 // get the return code from the process 674 return process.waitFor(); 675 } 676 677 678 679 /** 680 * Returns the {@link Throwable#getMessage()}. If the {@link Throwable#getMessage()} returns 681 * <code>null</code>, the method is called again on the cause of the Throwable object. 682 * <p/>If no Throwable in the chain has a valid message, the canonical name of the first 683 * exception is returned. 684 */ getExceptionMessage(Throwable t)685 static String getExceptionMessage(Throwable t) { 686 String message = t.getMessage(); 687 if (message == null) { 688 // no error info? get the stack call to display it 689 // At least that'll give us a better bug report. 690 ByteArrayOutputStream baos = new ByteArrayOutputStream(); 691 t.printStackTrace(new PrintStream(baos)); 692 message = baos.toString(); 693 } 694 695 return message; 696 } 697 } 698