2 * Copyright (C) 2008 The Android Open Source Project
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
8 * http://www.eclipse.org/org/documents/epl-v10.php
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.
17 package com.android.ide.eclipse.adt.internal.wizards.export;
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;
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;
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;
58 import java.util.Map.Entry;
61 * Final page of the wizard that checks the key and ask for the ouput location.
63 final class KeyCheckPage extends ExportWizardPage {
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;
75 private String mKeyDetails;
76 private String mDestinationDetails;
78 protected KeyCheckPage(ExportWizard wizard, String pageName) {
82 setTitle("Destination and key/certificate checks");
83 setDescription(""); // TODO
86 public void createControl(Composite parent) {
87 setErrorMessage(null);
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);
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*/);
107 final Button browseButton = new Button(composite, SWT.PUSH);
108 browseButton.setText("Browse...");
109 browseButton.addSelectionListener(new SelectionAdapter() {
111 public void widgetSelected(SelectionEvent e) {
112 FileDialog fileDialog = new FileDialog(browseButton.getShell(), SWT.SAVE);
114 fileDialog.setText("Destination file name");
115 // get a default apk name based on the project
116 String filename = ProjectHelper.getApkFilename(mWizard.getProject(),
118 fileDialog.setFileName(filename);
120 String saveLocation = fileDialog.open();
121 if (saveLocation != null) {
122 mDestination.setText(saveLocation);
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);
133 mDetailText = new FormText(mScrolledComposite, SWT.NONE);
134 mScrolledComposite.setContent(mDetailText);
136 mScrolledComposite.addControlListener(new ControlAdapter() {
138 public void controlResized(ControlEvent e) {
143 setControl(composite);
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);
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);
163 // if anything change we basically reload the data.
164 if (mProjectDataChanged != 0) {
165 mFatalSigningError = false;
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);
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>",
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>");
188 mKeyDetails = sb.toString();
191 KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
192 FileInputStream fis = new FileInputStream(mWizard.getKeystore());
193 keyStore.load(fis, mWizard.getKeystorePassword().toCharArray());
195 PrivateKeyEntry entry = (KeyStore.PrivateKeyEntry)keyStore.getEntry(
196 mWizard.getKeyAlias(),
197 new KeyStore.PasswordProtection(
198 mWizard.getKeyPassword().toCharArray()));
201 mPrivateKey = entry.getPrivateKey();
202 mCertificate = (X509Certificate)entry.getCertificate();
204 setErrorMessage("Unable to find key.");
206 setPageComplete(false);
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.
212 } catch (KeyStoreException e) {
214 } catch (NoSuchAlgorithmException e) {
216 } catch (UnrecoverableEntryException e) {
218 } catch (CertificateException e) {
220 } catch (IOException e) {
224 if (mPrivateKey != null && mCertificate != null) {
225 Calendar expirationCalendar = Calendar.getInstance();
226 expirationCalendar.setTime(mCertificate.getNotAfter());
227 Calendar today = Calendar.getInstance();
229 if (expirationCalendar.before(today)) {
230 mKeyDetails = String.format(
231 "<p>Certificate expired on %s</p>",
232 mCertificate.getNotAfter().toString());
234 // fatal error = nothing can make the page complete.
235 mFatalSigningError = true;
237 setErrorMessage("Certificate is expired.");
238 setPageComplete(false);
240 // valid, key/cert: put it in the wizard so that it can be finished
241 mWizard.setSigningInfo(mPrivateKey, mCertificate);
243 StringBuilder sb = new StringBuilder(String.format(
244 "<p>Certificate expires on %s.</p>",
245 mCertificate.getNotAfter().toString()));
247 int expirationYear = expirationCalendar.get(Calendar.YEAR);
248 int thisYear = today.get(Calendar.YEAR);
250 if (thisYear + 25 < expirationYear) {
253 if (expirationYear == thisYear) {
254 sb.append("<p>The certificate expires this year.</p>");
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"));
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>");
269 mKeyDetails = sb.toString();
272 // fatal error = nothing can make the page complete.
273 mFatalSigningError = true;
278 onDestinationChange(true /*forceDetailUpdate*/);
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.
286 private void onDestinationChange(boolean forceDetailUpdate) {
287 if (mFatalSigningError == false) {
288 // reset messages for now.
289 setErrorMessage(null);
292 String path = mDestination.getText().trim();
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);
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);
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);
320 // display the list of files that will actually be created
321 Map<String, String[]> apkFileMap = getApkFileMap(file);
324 boolean fileExists = false;
325 StringBuilder sb = new StringBuilder(String.format(
326 "<p>This will create the following files:</p>"));
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);
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);
343 sb.append(String.format("<li>%1$s</li>", filename));
347 mDestinationDetails = sb.toString();
349 // no error, set the destination in the wizard.
350 mWizard.setDestination(parentFolder, apkFileMap);
351 setPageComplete(true);
353 // However, we should also test if the file already exists.
355 setMessage("A destination file already exists.", WARNING);
359 } else if (forceDetailUpdate) {
365 * Updates the scrollbar to match the content of the {@link FormText} or the new size
366 * of the {@link ScrolledComposite}.
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();
376 private void updateDetailText() {
377 StringBuilder sb = new StringBuilder("<form>");
378 if (mKeyDetails != null) {
379 sb.append(mKeyDetails);
382 if (mDestinationDetails != null && mFatalSigningError == false) {
383 sb.append(mDestinationDetails);
386 sb.append("</form>");
388 mDetailText.setText(sb.toString(), true /* parseTags */,
389 true /* expandURLs */);
391 mDetailText.getParent().layout();
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.
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.
405 private Map<String, String[]> getApkFileMap(File file) {
406 String filename = file.getName();
408 HashMap<String, String[]> map = new HashMap<String, String[]>();
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);
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);
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);
438 protected void onException(Throwable t) {
439 super.onException(t);
441 mKeyDetails = String.format("ERROR: %1$s", ExportWizard.getExceptionMessage(t));