blob: c17f43e3806f90efff64cf90001ecad6ea6e3dd7 [file] [log] [blame]
/*
* 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));
}
}