aboutsummaryrefslogtreecommitdiff
path: root/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/export/KeyCheckPage.java
diff options
context:
space:
mode:
Diffstat (limited to 'eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/export/KeyCheckPage.java')
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/export/KeyCheckPage.java378
1 files changed, 378 insertions, 0 deletions
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/export/KeyCheckPage.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/export/KeyCheckPage.java
new file mode 100644
index 000000000..c17f43e38
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/export/KeyCheckPage.java
@@ -0,0 +1,378 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.eclipse.adt.internal.wizards.export;
+
+import com.android.ide.eclipse.adt.internal.project.ProjectHelper;
+import com.android.ide.eclipse.adt.internal.wizards.export.ExportWizard.ExportWizardPage;
+
+import org.eclipse.core.resources.IProject;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.custom.ScrolledComposite;
+import org.eclipse.swt.events.ControlAdapter;
+import org.eclipse.swt.events.ControlEvent;
+import org.eclipse.swt.events.ModifyEvent;
+import org.eclipse.swt.events.ModifyListener;
+import org.eclipse.swt.events.SelectionAdapter;
+import org.eclipse.swt.events.SelectionEvent;
+import org.eclipse.swt.graphics.Rectangle;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.layout.GridLayout;
+import org.eclipse.swt.widgets.Button;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.FileDialog;
+import org.eclipse.swt.widgets.Label;
+import org.eclipse.swt.widgets.Text;
+import org.eclipse.ui.forms.widgets.FormText;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.security.KeyStore;
+import java.security.KeyStore.PrivateKeyEntry;
+import java.security.KeyStoreException;
+import java.security.NoSuchAlgorithmException;
+import java.security.PrivateKey;
+import java.security.UnrecoverableEntryException;
+import java.security.cert.CertificateException;
+import java.security.cert.X509Certificate;
+import java.util.Calendar;
+
+/**
+ * Final page of the wizard that checks the key and ask for the ouput location.
+ */
+final class KeyCheckPage extends ExportWizardPage {
+
+ private static final int REQUIRED_YEARS = 25;
+
+ private static final String VALIDITY_WARNING =
+ "<p>Make sure the certificate is valid for the planned lifetime of the product.</p>"
+ + "<p>If the certificate expires, you will be forced to sign your application with "
+ + "a different one.</p>"
+ + "<p>Applications cannot be upgraded if their certificate changes from "
+ + "one version to another, forcing a full uninstall/install, which will make "
+ + "the user lose his/her data.</p>"
+ + "<p>Google Play(Android Market) currently requires certificates to be valid "
+ + "until 2033.</p>";
+
+ private final ExportWizard mWizard;
+ private PrivateKey mPrivateKey;
+ private X509Certificate mCertificate;
+ private Text mDestination;
+ private boolean mFatalSigningError;
+ private FormText mDetailText;
+ private ScrolledComposite mScrolledComposite;
+
+ private String mKeyDetails;
+ private String mDestinationDetails;
+
+ protected KeyCheckPage(ExportWizard wizard, String pageName) {
+ super(pageName);
+ mWizard = wizard;
+
+ setTitle("Destination and key/certificate checks");
+ setDescription(""); // TODO
+ }
+
+ @Override
+ public void createControl(Composite parent) {
+ setErrorMessage(null);
+ setMessage(null);
+
+ // build the ui.
+ Composite composite = new Composite(parent, SWT.NULL);
+ composite.setLayoutData(new GridData(GridData.FILL_BOTH));
+ GridLayout gl = new GridLayout(3, false);
+ gl.verticalSpacing *= 3;
+ composite.setLayout(gl);
+
+ GridData gd;
+
+ new Label(composite, SWT.NONE).setText("Destination APK file:");
+ mDestination = new Text(composite, SWT.BORDER);
+ mDestination.setLayoutData(gd = new GridData(GridData.FILL_HORIZONTAL));
+ mDestination.addModifyListener(new ModifyListener() {
+ @Override
+ public void modifyText(ModifyEvent e) {
+ onDestinationChange(false /*forceDetailUpdate*/);
+ }
+ });
+ final Button browseButton = new Button(composite, SWT.PUSH);
+ browseButton.setText("Browse...");
+ browseButton.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent e) {
+ FileDialog fileDialog = new FileDialog(browseButton.getShell(), SWT.SAVE);
+
+ fileDialog.setText("Destination file name");
+ // get a default apk name based on the project
+ String filename = ProjectHelper.getApkFilename(mWizard.getProject(),
+ null /*config*/);
+ fileDialog.setFileName(filename);
+
+ String saveLocation = fileDialog.open();
+ if (saveLocation != null) {
+ mDestination.setText(saveLocation);
+ }
+ }
+ });
+
+ mScrolledComposite = new ScrolledComposite(composite, SWT.V_SCROLL);
+ mScrolledComposite.setLayoutData(gd = new GridData(GridData.FILL_BOTH));
+ gd.horizontalSpan = 3;
+ mScrolledComposite.setExpandHorizontal(true);
+ mScrolledComposite.setExpandVertical(true);
+
+ mDetailText = new FormText(mScrolledComposite, SWT.NONE);
+ mScrolledComposite.setContent(mDetailText);
+
+ mScrolledComposite.addControlListener(new ControlAdapter() {
+ @Override
+ public void controlResized(ControlEvent e) {
+ updateScrolling();
+ }
+ });
+
+ setControl(composite);
+ }
+
+ @Override
+ void onShow() {
+ // fill the texts with information loaded from the project.
+ if ((mProjectDataChanged & DATA_PROJECT) != 0) {
+ // reset the destination from the content of the project
+ IProject project = mWizard.getProject();
+
+ String destination = ProjectHelper.loadStringProperty(project,
+ ExportWizard.PROPERTY_DESTINATION);
+ if (destination != null) {
+ mDestination.setText(destination);
+ }
+ }
+
+ // if anything change we basically reload the data.
+ if (mProjectDataChanged != 0) {
+ mFatalSigningError = false;
+
+ // reset the wizard with no key/cert to make it not finishable, unless a valid
+ // key/cert is found.
+ mWizard.setSigningInfo(null, null);
+ mPrivateKey = null;
+ mCertificate = null;
+ mKeyDetails = null;
+
+ if (mWizard.getKeystoreCreationMode() || mWizard.getKeyCreationMode()) {
+ int validity = mWizard.getValidity();
+ StringBuilder sb = new StringBuilder(
+ String.format("<p>Certificate expires in %d years.</p>",
+ validity));
+
+ if (validity < REQUIRED_YEARS) {
+ sb.append(VALIDITY_WARNING);
+ }
+
+ mKeyDetails = sb.toString();
+ } else {
+ try {
+ KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
+ FileInputStream fis = new FileInputStream(mWizard.getKeystore());
+ keyStore.load(fis, mWizard.getKeystorePassword().toCharArray());
+ fis.close();
+ PrivateKeyEntry entry = (KeyStore.PrivateKeyEntry)keyStore.getEntry(
+ mWizard.getKeyAlias(),
+ new KeyStore.PasswordProtection(
+ mWizard.getKeyPassword().toCharArray()));
+
+ if (entry != null) {
+ mPrivateKey = entry.getPrivateKey();
+ mCertificate = (X509Certificate)entry.getCertificate();
+ } else {
+ setErrorMessage("Unable to find key.");
+
+ setPageComplete(false);
+ }
+ } catch (FileNotFoundException e) {
+ // this was checked at the first previous step and will not happen here, unless
+ // the file was removed during the export wizard execution.
+ onException(e);
+ } catch (KeyStoreException e) {
+ onException(e);
+ } catch (NoSuchAlgorithmException e) {
+ onException(e);
+ } catch (UnrecoverableEntryException e) {
+ onException(e);
+ } catch (CertificateException e) {
+ onException(e);
+ } catch (IOException e) {
+ onException(e);
+ }
+
+ if (mPrivateKey != null && mCertificate != null) {
+ Calendar expirationCalendar = Calendar.getInstance();
+ expirationCalendar.setTime(mCertificate.getNotAfter());
+ Calendar today = Calendar.getInstance();
+
+ if (expirationCalendar.before(today)) {
+ mKeyDetails = String.format(
+ "<p>Certificate expired on %s</p>",
+ mCertificate.getNotAfter().toString());
+
+ // fatal error = nothing can make the page complete.
+ mFatalSigningError = true;
+
+ setErrorMessage("Certificate is expired.");
+ setPageComplete(false);
+ } else {
+ // valid, key/cert: put it in the wizard so that it can be finished
+ mWizard.setSigningInfo(mPrivateKey, mCertificate);
+
+ StringBuilder sb = new StringBuilder(String.format(
+ "<p>Certificate expires on %s.</p>",
+ mCertificate.getNotAfter().toString()));
+
+ int expirationYear = expirationCalendar.get(Calendar.YEAR);
+ int thisYear = today.get(Calendar.YEAR);
+
+ if (thisYear + REQUIRED_YEARS < expirationYear) {
+ // do nothing
+ } else {
+ if (expirationYear == thisYear) {
+ sb.append("<p>The certificate expires this year.</p>");
+ } else {
+ int count = expirationYear-thisYear;
+ sb.append(String.format(
+ "<p>The Certificate expires in %1$s %2$s.</p>",
+ count, count == 1 ? "year" : "years"));
+ }
+ sb.append(VALIDITY_WARNING);
+ }
+
+ // show certificate fingerprints
+ String sha1 = mWizard.getCertSha1Fingerprint();
+ String md5 = mWizard.getCertMd5Fingerprint();
+
+ sb.append("<p></p>" /*blank line*/);
+ sb.append("<p>Certificate fingerprints:</p>");
+ sb.append(String.format("<li>MD5 : %s</li>", md5));
+ sb.append(String.format("<li>SHA1: %s</li>", sha1));
+ sb.append("<p></p>" /*blank line*/);
+
+ mKeyDetails = sb.toString();
+ }
+ } else {
+ // fatal error = nothing can make the page complete.
+ mFatalSigningError = true;
+ }
+ }
+ }
+
+ onDestinationChange(true /*forceDetailUpdate*/);
+ }
+
+ /**
+ * Callback for destination field edition
+ * @param forceDetailUpdate if true, the detail {@link FormText} is updated even if a fatal
+ * error has happened in the signing.
+ */
+ private void onDestinationChange(boolean forceDetailUpdate) {
+ if (mFatalSigningError == false) {
+ // reset messages for now.
+ setErrorMessage(null);
+ setMessage(null);
+
+ String path = mDestination.getText().trim();
+
+ if (path.length() == 0) {
+ setErrorMessage("Enter destination for the APK file.");
+ // reset canFinish in the wizard.
+ mWizard.resetDestination();
+ setPageComplete(false);
+ return;
+ }
+
+ File file = new File(path);
+ if (file.isDirectory()) {
+ setErrorMessage("Destination is a directory.");
+ // reset canFinish in the wizard.
+ mWizard.resetDestination();
+ setPageComplete(false);
+ return;
+ }
+
+ File parentFolder = file.getParentFile();
+ if (parentFolder == null || parentFolder.isDirectory() == false) {
+ setErrorMessage("Not a valid directory.");
+ // reset canFinish in the wizard.
+ mWizard.resetDestination();
+ setPageComplete(false);
+ return;
+ }
+
+ if (file.isFile()) {
+ mDestinationDetails = "<li>WARNING: destination file already exists</li>";
+ setMessage("Destination file already exists.", WARNING);
+ }
+
+ // no error, set the destination in the wizard.
+ mWizard.setDestination(file);
+ setPageComplete(true);
+
+ updateDetailText();
+ } else if (forceDetailUpdate) {
+ updateDetailText();
+ }
+ }
+
+ /**
+ * Updates the scrollbar to match the content of the {@link FormText} or the new size
+ * of the {@link ScrolledComposite}.
+ */
+ private void updateScrolling() {
+ if (mDetailText != null) {
+ Rectangle r = mScrolledComposite.getClientArea();
+ mScrolledComposite.setMinSize(mDetailText.computeSize(r.width, SWT.DEFAULT));
+ mScrolledComposite.layout();
+ }
+ }
+
+ private void updateDetailText() {
+ StringBuilder sb = new StringBuilder("<form>");
+ if (mKeyDetails != null) {
+ sb.append(mKeyDetails);
+ }
+
+ if (mDestinationDetails != null && mFatalSigningError == false) {
+ sb.append(mDestinationDetails);
+ }
+
+ sb.append("</form>");
+
+ mDetailText.setText(sb.toString(), true /* parseTags */,
+ true /* expandURLs */);
+
+ mDetailText.getParent().layout();
+
+ updateScrolling();
+ }
+
+ @Override
+ protected void onException(Throwable t) {
+ super.onException(t);
+
+ mKeyDetails = String.format("ERROR: %1$s", ExportWizard.getExceptionMessage(t));
+ }
+}