2 * Copyright (C) 2008 The Android Open Source Project
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
8 * http://www.eclipse.org/org/documents/epl-v10.php
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.
17 package com.android.ide.eclipse.adt.internal.wizards.export;
19 import com.android.ide.eclipse.adt.AdtPlugin;
20 import com.android.ide.eclipse.adt.internal.preferences.AdtPrefs.BuildVerbosity;
21 import com.android.ide.eclipse.adt.internal.project.BaseProjectHelper;
22 import com.android.ide.eclipse.adt.internal.project.ProjectHelper;
23 import com.android.sdklib.internal.build.KeystoreHelper;
24 import com.android.sdklib.internal.build.SignedJarBuilder;
25 import com.android.sdklib.internal.build.DebugKeyProvider.IKeyGenOutput;
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;
45 import java.io.BufferedReader;
46 import java.io.ByteArrayOutputStream;
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;
62 import java.util.Map.Entry;
65 * Export wizard to export an apk signed with a release key/certificate.
67 public final class ExportWizard extends Wizard implements IExportWizard {
69 private static final String PROJECT_LOGO_LARGE = "icons/android_large.png"; //$NON-NLS-1$
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$
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$
82 static final int APK_FILE_SOURCE = 0;
83 static final int APK_FILE_DEST = 1;
84 static final int APK_COUNT = 2;
87 * Base page class for the ExportWizard page. This class add the {@link #onShow()} callback.
89 static abstract class ExportWizardPage extends WizardPage {
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;
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();
103 // first limit to 127 characters max
104 if (len + ((Text)e.getSource()).getText().length() > 127) {
109 // now only take non control characters
110 for (int i = 0 ; i < len ; i++) {
111 if (e.text.charAt(i) < 32) {
120 * Bit mask indicating what changed while the page was hidden.
122 * @see #DATA_KEYSTORE
125 protected int mProjectDataChanged = 0;
127 ExportWizardPage(String name) {
131 abstract void onShow();
134 public void setVisible(boolean visible) {
135 super.setVisible(visible);
138 mProjectDataChanged = 0;
142 final void projectDataChanged(int changeMask) {
143 mProjectDataChanged |= changeMask;
147 * Calls {@link #setErrorMessage(String)} and {@link #setPageComplete(boolean)} based on a
148 * {@link Throwable} object.
150 protected void onException(Throwable t) {
151 String message = getExceptionMessage(t);
153 setErrorMessage(message);
154 setPageComplete(false);
158 private ExportWizardPage mPages[] = new ExportWizardPage[5];
160 private IProject mProject;
162 private String mKeystore;
163 private String mKeystorePassword;
164 private boolean mKeystoreCreationMode;
166 private String mKeyAlias;
167 private String mKeyPassword;
168 private int mValidity;
169 private String mDName;
171 private PrivateKey mPrivateKey;
172 private X509Certificate mCertificate;
174 private File mDestinationParentFolder;
176 private ExportWizardPage mKeystoreSelectionPage;
177 private ExportWizardPage mKeyCreationPage;
178 private ExportWizardPage mKeySelectionPage;
179 private ExportWizardPage mKeyCheckPage;
181 private boolean mKeyCreationMode;
183 private List<String> mExistingAliases;
185 private Map<String, String[]> mApkMap;
187 public ExportWizard() {
188 setHelpAvailable(false); // TODO have help
189 setWindowTitle("Export Android Application");
190 setImageDescriptor();
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));
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]);
213 // run the export in an UI runnable.
214 IWorkbench workbench = PlatformUI.getWorkbench();
215 final boolean[] result = new boolean[1];
217 workbench.getProgressService().busyCursorWhile(new IRunnableWithProgress() {
220 * @throws InvocationTargetException
221 * @throws InterruptedException
223 public void run(IProgressMonitor monitor) throws InvocationTargetException,
224 InterruptedException {
226 result[0] = doExport(monitor);
232 } catch (InvocationTargetException e) {
234 } catch (InterruptedException e) {
241 private boolean doExport(IProgressMonitor monitor) {
243 // first we make sure the project is built
244 mProject.build(IncrementalProjectBuilder.INCREMENTAL_BUILD, monitor);
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(
257 new IKeyGenOutput() {
258 public void err(String message) {
261 public void out(String message) {
266 if (createdStore == false) {
267 // keystore creation error!
268 displayError(output.toArray(new String[output.size()]));
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());
277 PrivateKeyEntry entry = (KeyStore.PrivateKeyEntry)keyStore.getEntry(
278 mKeyAlias, new KeyStore.PasswordProtection(mKeyPassword.toCharArray()));
281 mPrivateKey = entry.getPrivateKey();
282 mCertificate = (X509Certificate)entry.getCertificate();
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");
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) {
299 String outputOsPath = outputIFolder.getLocation().toOSString();
301 // now generate the packages.
302 Set<Entry<String, String[]>> set = mApkMap.entrySet();
304 boolean runZipAlign = false;
305 String path = AdtPlugin.getOsAbsoluteZipAlign();
306 File zipalign = new File(path);
307 runZipAlign = zipalign.isFile();
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];
315 destFile = File.createTempFile("android", ".apk");
317 destFile = new File(mDestinationParentFolder, destFilename);
321 FileOutputStream fos = new FileOutputStream(destFile);
322 SignedJarBuilder builder = new SignedJarBuilder(fos, mPrivateKey, mCertificate);
324 // get the input file.
325 FileInputStream fis = new FileInputStream(new File(outputOsPath, srcFilename));
327 // add the content of the source file to the output file, and sign it at
330 builder.writeZip(fis, null /* filter */);
331 // close the builder: write the final signature files,
332 // and close the archive.
335 // now zipalign the result
337 File realDestFile = new File(mDestinationParentFolder, destFilename);
338 String message = zipAlign(path, destFile, realDestFile);
339 if (message != null) {
340 displayError(message);
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.");
365 } catch (Throwable t) {
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;
386 * @see org.eclipse.ui.IWorkbenchWizard#init(org.eclipse.ui.IWorkbench, org.eclipse.jface.viewers.IStructuredSelection)
388 public void init(IWorkbench workbench, IStructuredSelection selection) {
389 // get the project from the selection
390 Object selected = selection.getFirstElement();
392 if (selected instanceof IProject) {
393 mProject = (IProject)selected;
394 } else if (selected instanceof IAdaptable) {
395 IResource r = (IResource)((IAdaptable)selected).getAdapter(IResource.class);
397 mProject = r.getProject();
402 ExportWizardPage getKeystoreSelectionPage() {
403 return mKeystoreSelectionPage;
406 ExportWizardPage getKeyCreationPage() {
407 return mKeyCreationPage;
410 ExportWizardPage getKeySelectionPage() {
411 return mKeySelectionPage;
414 ExportWizardPage getKeyCheckPage() {
415 return mKeyCheckPage;
419 * Returns an image descriptor for the wizard logo.
421 private void setImageDescriptor() {
422 ImageDescriptor desc = AdtPlugin.getImageDescriptor(PROJECT_LOGO_LARGE);
423 setDefaultPageImageDescriptor(desc);
426 IProject getProject() {
430 void setProject(IProject project) {
433 updatePageOnChange(ExportWizardPage.DATA_PROJECT);
436 void setKeystore(String path) {
441 updatePageOnChange(ExportWizardPage.DATA_KEYSTORE);
444 String getKeystore() {
448 void setKeystoreCreationMode(boolean createStore) {
449 mKeystoreCreationMode = createStore;
450 updatePageOnChange(ExportWizardPage.DATA_KEYSTORE);
453 boolean getKeystoreCreationMode() {
454 return mKeystoreCreationMode;
458 void setKeystorePassword(String password) {
459 mKeystorePassword = password;
463 updatePageOnChange(ExportWizardPage.DATA_KEYSTORE);
466 String getKeystorePassword() {
467 return mKeystorePassword;
470 void setKeyCreationMode(boolean createKey) {
471 mKeyCreationMode = createKey;
472 updatePageOnChange(ExportWizardPage.DATA_KEY);
475 boolean getKeyCreationMode() {
476 return mKeyCreationMode;
479 void setExistingAliases(List<String> aliases) {
480 mExistingAliases = aliases;
483 List<String> getExistingAliases() {
484 return mExistingAliases;
487 void setKeyAlias(String name) {
492 updatePageOnChange(ExportWizardPage.DATA_KEY);
495 String getKeyAlias() {
499 void setKeyPassword(String password) {
500 mKeyPassword = password;
504 updatePageOnChange(ExportWizardPage.DATA_KEY);
507 String getKeyPassword() {
511 void setValidity(int validity) {
512 mValidity = validity;
513 updatePageOnChange(ExportWizardPage.DATA_KEY);
520 void setDName(String dName) {
522 updatePageOnChange(ExportWizardPage.DATA_KEY);
529 void setSigningInfo(PrivateKey privateKey, X509Certificate certificate) {
530 mPrivateKey = privateKey;
531 mCertificate = certificate;
534 void setDestination(File parentFolder, Map<String, String[]> apkMap) {
535 mDestinationParentFolder = parentFolder;
539 void resetDestination() {
540 mDestinationParentFolder = null;
544 void updatePageOnChange(int changeMask) {
545 for (ExportWizardPage page : mPages) {
546 page.projectDataChanged(changeMask);
550 private void displayError(String... messages) {
551 String message = null;
552 if (messages.length == 1) {
553 message = messages[0];
555 StringBuilder sb = new StringBuilder(messages[0]);
556 for (int i = 1; i < messages.length; i++) {
558 sb.append(messages[i]);
561 message = sb.toString();
564 AdtPlugin.displayError("Export Wizard", message);
567 private void displayError(Throwable t) {
568 String message = getExceptionMessage(t);
569 displayError(message);
571 AdtPlugin.log(t, "Export Wizard Error");
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
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();
591 Process process = Runtime.getRuntime().exec(command);
592 ArrayList<String> output = new ArrayList<String>();
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) {
602 return sb.toString();
604 } catch (InterruptedException e) {
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
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.
624 // read the lines as they come. if null is returned, it's
625 // because the process finished
626 new Thread("") { //$NON-NLS-1$
629 // create a buffer to read the stderr output
630 InputStreamReader is = new InputStreamReader(process.getErrorStream());
631 BufferedReader errReader = new BufferedReader(is);
635 String line = errReader.readLine();
642 } catch (IOException e) {
648 new Thread("") { //$NON-NLS-1$
651 InputStreamReader is = new InputStreamReader(process.getInputStream());
652 BufferedReader outReader = new BufferedReader(is);
654 IProject project = getProject();
658 String line = outReader.readLine();
660 AdtPlugin.printBuildToConsole(BuildVerbosity.VERBOSE,
666 } catch (IOException e) {
673 // get the return code from the process
674 return process.waitFor();
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.
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();