OSDN Git Service

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