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