Home | History | Annotate | Download | only in export
      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 
    127         ExportWizardPage(String name) {
    128             super(name);
    129         }
    130 
    131         abstract void onShow();
    132 
    133         @Override
    134         public void setVisible(boolean visible) {
    135             super.setVisible(visible);
    136             if (visible) {
    137                 onShow();
    138                 mProjectDataChanged = 0;
    139             }
    140         }
    141 
    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          */
    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 
    185     public ExportWizard() {
    186         setHelpAvailable(false); // TODO have help
    187         setWindowTitle("Export Android Application");
    188         setImageDescriptor();
    189     }
    190 
    191     @Override
    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
    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 
    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
    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
    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 
    372     ExportWizardPage getKeystoreSelectionPage() {
    373         return mKeystoreSelectionPage;
    374     }
    375 
    376     ExportWizardPage getKeyCreationPage() {
    377         return mKeyCreationPage;
    378     }
    379 
    380     ExportWizardPage getKeySelectionPage() {
    381         return mKeySelectionPage;
    382     }
    383 
    384     ExportWizardPage getKeyCheckPage() {
    385         return mKeyCheckPage;
    386     }
    387 
    388     /**
    389      * Returns an image descriptor for the wizard logo.
    390      */
    391     private void setImageDescriptor() {
    392         ImageDescriptor desc = AdtPlugin.getImageDescriptor(PROJECT_LOGO_LARGE);
    393         setDefaultPageImageDescriptor(desc);
    394     }
    395 
    396     IProject getProject() {
    397         return mProject;
    398     }
    399 
    400     void setProject(IProject project) {
    401         mProject = project;
    402 
    403         updatePageOnChange(ExportWizardPage.DATA_PROJECT);
    404     }
    405 
    406     void setKeystore(String path) {
    407         mKeystore = path;
    408         mPrivateKey = null;
    409         mCertificate = null;
    410 
    411         updatePageOnChange(ExportWizardPage.DATA_KEYSTORE);
    412     }
    413 
    414     String getKeystore() {
    415         return mKeystore;
    416     }
    417 
    418     void setKeystoreCreationMode(boolean createStore) {
    419         mKeystoreCreationMode = createStore;
    420         updatePageOnChange(ExportWizardPage.DATA_KEYSTORE);
    421     }
    422 
    423     boolean getKeystoreCreationMode() {
    424         return mKeystoreCreationMode;
    425     }
    426 
    427 
    428     void setKeystorePassword(String password) {
    429         mKeystorePassword = password;
    430         mPrivateKey = null;
    431         mCertificate = null;
    432 
    433         updatePageOnChange(ExportWizardPage.DATA_KEYSTORE);
    434     }
    435 
    436     String getKeystorePassword() {
    437         return mKeystorePassword;
    438     }
    439 
    440     void setKeyCreationMode(boolean createKey) {
    441         mKeyCreationMode = createKey;
    442         updatePageOnChange(ExportWizardPage.DATA_KEY);
    443     }
    444 
    445     boolean getKeyCreationMode() {
    446         return mKeyCreationMode;
    447     }
    448 
    449     void setExistingAliases(List<String> aliases) {
    450         mExistingAliases = aliases;
    451     }
    452 
    453     List<String> getExistingAliases() {
    454         return mExistingAliases;
    455     }
    456 
    457     void setKeyAlias(String name) {
    458         mKeyAlias = name;
    459         mPrivateKey = null;
    460         mCertificate = null;
    461 
    462         updatePageOnChange(ExportWizardPage.DATA_KEY);
    463     }
    464 
    465     String getKeyAlias() {
    466         return mKeyAlias;
    467     }
    468 
    469     void setKeyPassword(String password) {
    470         mKeyPassword = password;
    471         mPrivateKey = null;
    472         mCertificate = null;
    473 
    474         updatePageOnChange(ExportWizardPage.DATA_KEY);
    475     }
    476 
    477     String getKeyPassword() {
    478         return mKeyPassword;
    479     }
    480 
    481     void setValidity(int validity) {
    482         mValidity = validity;
    483         updatePageOnChange(ExportWizardPage.DATA_KEY);
    484     }
    485 
    486     int getValidity() {
    487         return mValidity;
    488     }
    489 
    490     void setDName(String dName) {
    491         mDName = dName;
    492         updatePageOnChange(ExportWizardPage.DATA_KEY);
    493     }
    494 
    495     String getDName() {
    496         return mDName;
    497     }
    498 
    499     String getCertSha1Fingerprint() {
    500         return FingerprintUtils.getFingerprint(mCertificate, "SHA1");
    501     }
    502 
    503     String getCertMd5Fingerprint() {
    504         return FingerprintUtils.getFingerprint(mCertificate, "MD5");
    505     }
    506 
    507     void setSigningInfo(PrivateKey privateKey, X509Certificate certificate) {
    508         mPrivateKey = privateKey;
    509         mCertificate = certificate;
    510     }
    511 
    512     void setDestination(File destinationFile) {
    513         mDestinationFile = destinationFile;
    514     }
    515 
    516     void resetDestination() {
    517         mDestinationFile = null;
    518     }
    519 
    520     void updatePageOnChange(int changeMask) {
    521         for (ExportWizardPage page : mPages) {
    522             page.projectDataChanged(changeMask);
    523         }
    524     }
    525 
    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 
    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      */
    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      */
    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