diff options
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.java | 378 |
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)); + } +} |