OSDN Git Service

am 7e056ae8: AI 148870: Pinging Ryan for Dr No approval. --- Cloned from CL 14724...
[android-x86/sdk.git] / eclipse / plugins / com.android.ide.eclipse.adt / src / com / android / ide / eclipse / adt / internal / wizards / export / KeyCheckPage.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.internal.project.ProjectHelper;
20 import com.android.ide.eclipse.adt.internal.sdk.Sdk;
21 import com.android.ide.eclipse.adt.internal.wizards.export.ExportWizard.ExportWizardPage;
22
23 import org.eclipse.core.resources.IProject;
24 import org.eclipse.swt.SWT;
25 import org.eclipse.swt.custom.ScrolledComposite;
26 import org.eclipse.swt.events.ControlAdapter;
27 import org.eclipse.swt.events.ControlEvent;
28 import org.eclipse.swt.events.ModifyEvent;
29 import org.eclipse.swt.events.ModifyListener;
30 import org.eclipse.swt.events.SelectionAdapter;
31 import org.eclipse.swt.events.SelectionEvent;
32 import org.eclipse.swt.graphics.Rectangle;
33 import org.eclipse.swt.layout.GridData;
34 import org.eclipse.swt.layout.GridLayout;
35 import org.eclipse.swt.widgets.Button;
36 import org.eclipse.swt.widgets.Composite;
37 import org.eclipse.swt.widgets.FileDialog;
38 import org.eclipse.swt.widgets.Label;
39 import org.eclipse.swt.widgets.Text;
40 import org.eclipse.ui.forms.widgets.FormText;
41
42 import java.io.File;
43 import java.io.FileInputStream;
44 import java.io.FileNotFoundException;
45 import java.io.IOException;
46 import java.security.KeyStore;
47 import java.security.KeyStoreException;
48 import java.security.NoSuchAlgorithmException;
49 import java.security.PrivateKey;
50 import java.security.UnrecoverableEntryException;
51 import java.security.KeyStore.PrivateKeyEntry;
52 import java.security.cert.CertificateException;
53 import java.security.cert.X509Certificate;
54 import java.util.Calendar;
55 import java.util.HashMap;
56 import java.util.Map;
57 import java.util.Set;
58 import java.util.Map.Entry;
59
60 /**
61  * Final page of the wizard that checks the key and ask for the ouput location.
62  */
63 final class KeyCheckPage extends ExportWizardPage {
64
65     private final ExportWizard mWizard;
66     private PrivateKey mPrivateKey;
67     private X509Certificate mCertificate;
68     private Text mDestination;
69     private boolean mFatalSigningError;
70     private FormText mDetailText;
71     /** The Apk Config map for the current project */
72     private Map<String, String> mApkConfig;
73     private ScrolledComposite mScrolledComposite;
74     
75     private String mKeyDetails;
76     private String mDestinationDetails;
77
78     protected KeyCheckPage(ExportWizard wizard, String pageName) {
79         super(pageName);
80         mWizard = wizard;
81         
82         setTitle("Destination and key/certificate checks");
83         setDescription(""); // TODO
84     }
85
86     public void createControl(Composite parent) {
87         setErrorMessage(null);
88         setMessage(null);
89
90         // build the ui.
91         Composite composite = new Composite(parent, SWT.NULL);
92         composite.setLayoutData(new GridData(GridData.FILL_BOTH));
93         GridLayout gl = new GridLayout(3, false);
94         gl.verticalSpacing *= 3;
95         composite.setLayout(gl);
96         
97         GridData gd;
98
99         new Label(composite, SWT.NONE).setText("Destination APK file:");
100         mDestination = new Text(composite, SWT.BORDER);
101         mDestination.setLayoutData(gd = new GridData(GridData.FILL_HORIZONTAL));
102         mDestination.addModifyListener(new ModifyListener() {
103             public void modifyText(ModifyEvent e) {
104                 onDestinationChange(false /*forceDetailUpdate*/);
105             }
106         });
107         final Button browseButton = new Button(composite, SWT.PUSH);
108         browseButton.setText("Browse...");
109         browseButton.addSelectionListener(new SelectionAdapter() {
110             @Override
111             public void widgetSelected(SelectionEvent e) {
112                 FileDialog fileDialog = new FileDialog(browseButton.getShell(), SWT.SAVE);
113                 
114                 fileDialog.setText("Destination file name");
115                 // get a default apk name based on the project
116                 String filename = ProjectHelper.getApkFilename(mWizard.getProject(),
117                         null /*config*/);
118                 fileDialog.setFileName(filename);
119         
120                 String saveLocation = fileDialog.open();
121                 if (saveLocation != null) {
122                     mDestination.setText(saveLocation);
123                 }
124             }
125         });
126         
127         mScrolledComposite = new ScrolledComposite(composite, SWT.V_SCROLL);
128         mScrolledComposite.setLayoutData(gd = new GridData(GridData.FILL_BOTH));
129         gd.horizontalSpan = 3;
130         mScrolledComposite.setExpandHorizontal(true);
131         mScrolledComposite.setExpandVertical(true);
132         
133         mDetailText = new FormText(mScrolledComposite, SWT.NONE);
134         mScrolledComposite.setContent(mDetailText);
135
136         mScrolledComposite.addControlListener(new ControlAdapter() {
137             @Override
138             public void controlResized(ControlEvent e) {
139                 updateScrolling();
140             }
141         });
142         
143         setControl(composite);
144     }
145     
146     @Override
147     void onShow() {
148         // fill the texts with information loaded from the project.
149         if ((mProjectDataChanged & DATA_PROJECT) != 0) {
150             // reset the destination from the content of the project
151             IProject project = mWizard.getProject();
152             mApkConfig = Sdk.getCurrent().getProjectApkConfigs(project);
153             
154             String destination = ProjectHelper.loadStringProperty(project,
155                     ExportWizard.PROPERTY_DESTINATION);
156             String filename = ProjectHelper.loadStringProperty(project,
157                     ExportWizard.PROPERTY_FILENAME);
158             if (destination != null && filename != null) {
159                 mDestination.setText(destination + File.separator + filename);
160             }
161         }
162         
163         // if anything change we basically reload the data.
164         if (mProjectDataChanged != 0) {
165             mFatalSigningError = false;
166
167             // reset the wizard with no key/cert to make it not finishable, unless a valid
168             // key/cert is found.
169             mWizard.setSigningInfo(null, null);
170             mPrivateKey = null;
171             mCertificate = null;
172             mKeyDetails = null;
173     
174             if (mWizard.getKeystoreCreationMode() || mWizard.getKeyCreationMode()) {
175                 int validity = mWizard.getValidity();
176                 StringBuilder sb = new StringBuilder(
177                         String.format("<p>Certificate expires in %d years.</p>",
178                         validity));
179
180                 if (validity < 25) {
181                     sb.append("<p>Make sure the certificate is valid for the planned lifetime of the product.</p>");
182                     sb.append("<p>If the certificate expires, you will be forced to sign your application with a different one.</p>");
183                     sb.append("<p>Applications cannot be upgraded if their certificate changes from one version to another, ");
184                     sb.append("forcing a full uninstall/install, which will make the user lose his/her data.</p>");
185                     sb.append("<p>Android Market currently requires certificates to be valid until 2033.</p>");
186                 }
187
188                 mKeyDetails = sb.toString();
189             } else {
190                 try {
191                     KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
192                     FileInputStream fis = new FileInputStream(mWizard.getKeystore());
193                     keyStore.load(fis, mWizard.getKeystorePassword().toCharArray());
194                     fis.close();
195                     PrivateKeyEntry entry = (KeyStore.PrivateKeyEntry)keyStore.getEntry(
196                             mWizard.getKeyAlias(),
197                             new KeyStore.PasswordProtection(
198                                     mWizard.getKeyPassword().toCharArray()));
199                     
200                     if (entry != null) {
201                         mPrivateKey = entry.getPrivateKey();
202                         mCertificate = (X509Certificate)entry.getCertificate();
203                     } else {
204                         setErrorMessage("Unable to find key.");
205                         
206                         setPageComplete(false);
207                     }
208                 } catch (FileNotFoundException e) {
209                     // this was checked at the first previous step and will not happen here, unless
210                     // the file was removed during the export wizard execution.
211                     onException(e);
212                 } catch (KeyStoreException e) {
213                     onException(e);
214                 } catch (NoSuchAlgorithmException e) {
215                     onException(e);
216                 } catch (UnrecoverableEntryException e) {
217                     onException(e);
218                 } catch (CertificateException e) {
219                     onException(e);
220                 } catch (IOException e) {
221                     onException(e);
222                 }
223                 
224                 if (mPrivateKey != null && mCertificate != null) {
225                     Calendar expirationCalendar = Calendar.getInstance();
226                     expirationCalendar.setTime(mCertificate.getNotAfter());
227                     Calendar today = Calendar.getInstance();
228                     
229                     if (expirationCalendar.before(today)) {
230                         mKeyDetails = String.format(
231                                 "<p>Certificate expired on %s</p>",
232                                 mCertificate.getNotAfter().toString());
233                         
234                         // fatal error = nothing can make the page complete.
235                         mFatalSigningError = true;
236         
237                         setErrorMessage("Certificate is expired.");
238                         setPageComplete(false);
239                     } else {
240                         // valid, key/cert: put it in the wizard so that it can be finished
241                         mWizard.setSigningInfo(mPrivateKey, mCertificate);
242         
243                         StringBuilder sb = new StringBuilder(String.format(
244                                 "<p>Certificate expires on %s.</p>",
245                                 mCertificate.getNotAfter().toString()));
246                         
247                         int expirationYear = expirationCalendar.get(Calendar.YEAR);
248                         int thisYear = today.get(Calendar.YEAR);
249                         
250                         if (thisYear + 25 < expirationYear) {
251                             // do nothing
252                         } else {
253                             if (expirationYear == thisYear) {
254                                 sb.append("<p>The certificate expires this year.</p>");
255                             } else {
256                                 int count = expirationYear-thisYear;
257                                 sb.append(String.format(
258                                         "<p>The Certificate expires in %1$s %2$s.</p>",
259                                         count, count == 1 ? "year" : "years"));
260                             }
261                             
262                             sb.append("<p>Make sure the certificate is valid for the planned lifetime of the product.</p>");
263                             sb.append("<p>If the certificate expires, you will be forced to sign your application with a different one.</p>");
264                             sb.append("<p>Applications cannot be upgraded if their certificate changes from one version to another, ");
265                             sb.append("forcing a full uninstall/install, which will make the user lose his/her data.</p>");
266                             sb.append("<p>Android Market currently requires certificates to be valid until 2033.</p>");
267                         }
268                         
269                         mKeyDetails = sb.toString();
270                     }
271                 } else {
272                     // fatal error = nothing can make the page complete.
273                     mFatalSigningError = true;
274                 }
275             }
276         }
277
278         onDestinationChange(true /*forceDetailUpdate*/);
279     }
280     
281     /**
282      * Callback for destination field edition
283      * @param forceDetailUpdate if true, the detail {@link FormText} is updated even if a fatal
284      * error has happened in the signing.
285      */
286     private void onDestinationChange(boolean forceDetailUpdate) {
287         if (mFatalSigningError == false) {
288             // reset messages for now.
289             setErrorMessage(null);
290             setMessage(null);
291
292             String path = mDestination.getText().trim();
293
294             if (path.length() == 0) {
295                 setErrorMessage("Enter destination for the APK file.");
296                 // reset canFinish in the wizard.
297                 mWizard.resetDestination();
298                 setPageComplete(false);
299                 return;
300             }
301
302             File file = new File(path);
303             if (file.isDirectory()) {
304                 setErrorMessage("Destination is a directory.");
305                 // reset canFinish in the wizard.
306                 mWizard.resetDestination();
307                 setPageComplete(false);
308                 return;
309             }
310
311             File parentFolder = file.getParentFile();
312             if (parentFolder == null || parentFolder.isDirectory() == false) {
313                 setErrorMessage("Not a valid directory.");
314                 // reset canFinish in the wizard.
315                 mWizard.resetDestination();
316                 setPageComplete(false);
317                 return;
318             }
319
320             // display the list of files that will actually be created
321             Map<String, String[]> apkFileMap = getApkFileMap(file);
322             
323             // display them
324             boolean fileExists = false;
325             StringBuilder sb = new StringBuilder(String.format(
326                     "<p>This will create the following files:</p>"));
327             
328             Set<Entry<String, String[]>> set = apkFileMap.entrySet();
329             for (Entry<String, String[]> entry : set) {
330                 String[] apkArray = entry.getValue();
331                 String filename = apkArray[ExportWizard.APK_FILE_DEST];
332                 File f = new File(parentFolder, filename);
333                 if (f.isFile()) {
334                     fileExists = true;
335                     sb.append(String.format("<li>%1$s (WARNING: already exists)</li>", filename));
336                 } else if (f.isDirectory()) {
337                     setErrorMessage(String.format("%1$s is a directory.", filename));
338                     // reset canFinish in the wizard.
339                     mWizard.resetDestination();
340                     setPageComplete(false);
341                     return;
342                 } else {
343                     sb.append(String.format("<li>%1$s</li>", filename));
344                 }
345             }
346
347             mDestinationDetails = sb.toString();
348
349             // no error, set the destination in the wizard.
350             mWizard.setDestination(parentFolder, apkFileMap);
351             setPageComplete(true);
352
353             // However, we should also test if the file already exists.
354             if (fileExists) {
355                 setMessage("A destination file already exists.", WARNING);
356             }
357
358             updateDetailText();
359         } else if (forceDetailUpdate) {
360             updateDetailText();
361         }
362     }
363     
364     /**
365      * Updates the scrollbar to match the content of the {@link FormText} or the new size
366      * of the {@link ScrolledComposite}.
367      */
368     private void updateScrolling() {
369         if (mDetailText != null) {
370             Rectangle r = mScrolledComposite.getClientArea();
371             mScrolledComposite.setMinSize(mDetailText.computeSize(r.width, SWT.DEFAULT));
372             mScrolledComposite.layout();
373         }
374     }
375     
376     private void updateDetailText() {
377         StringBuilder sb = new StringBuilder("<form>");
378         if (mKeyDetails != null) {
379             sb.append(mKeyDetails);
380         }
381         
382         if (mDestinationDetails != null && mFatalSigningError == false) {
383             sb.append(mDestinationDetails);
384         }
385         
386         sb.append("</form>");
387         
388         mDetailText.setText(sb.toString(), true /* parseTags */,
389                 true /* expandURLs */);
390
391         mDetailText.getParent().layout();
392
393         updateScrolling();
394
395     }
396
397     /**
398      * Creates the list of destination filenames based on the content of the destination field
399      * and the list of APK configurations for the project.
400      * 
401      * @param file File name from the destination field
402      * @return A list of destination filenames based <code>file</code> and the list of APK
403      *         configurations for the project.
404      */
405     private Map<String, String[]> getApkFileMap(File file) {
406         String filename = file.getName();
407         
408         HashMap<String, String[]> map = new HashMap<String, String[]>();
409         
410         // add the default APK filename
411         String[] apkArray = new String[ExportWizard.APK_COUNT];
412         apkArray[ExportWizard.APK_FILE_SOURCE] = ProjectHelper.getApkFilename(
413                 mWizard.getProject(), null /*config*/);
414         apkArray[ExportWizard.APK_FILE_DEST] = filename;
415         map.put(null, apkArray);
416
417         // add the APKs for each APK configuration.
418         if (mApkConfig != null && mApkConfig.size() > 0) {
419             // remove the extension.
420             int index = filename.lastIndexOf('.');
421             String base = filename.substring(0, index);
422             String extension = filename.substring(index);
423             
424             Set<Entry<String, String>> set = mApkConfig.entrySet();
425             for (Entry<String, String> entry : set) {
426                 apkArray = new String[ExportWizard.APK_COUNT];
427                 apkArray[ExportWizard.APK_FILE_SOURCE] = ProjectHelper.getApkFilename(
428                         mWizard.getProject(), entry.getKey());
429                 apkArray[ExportWizard.APK_FILE_DEST] = base + "-" + entry.getKey() + extension;
430                 map.put(entry.getKey(), apkArray);
431             }
432         }
433         
434         return map;
435     }
436     
437     @Override
438     protected void onException(Throwable t) {
439         super.onException(t);
440         
441         mKeyDetails = String.format("ERROR: %1$s", ExportWizard.getExceptionMessage(t));
442     }
443 }