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.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