blob: 0f630c4b83e1afb3ec1e01fbbbf135e8aa8c2600 [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.sdk.Sdk;
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.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.UnrecoverableEntryException;
import java.security.KeyStore.PrivateKeyEntry;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.Calendar;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.Map.Entry;
/**
* Final page of the wizard that checks the key and ask for the ouput location.
*/
final class KeyCheckPage extends ExportWizardPage {
private final ExportWizard mWizard;
private PrivateKey mPrivateKey;
private X509Certificate mCertificate;
private Text mDestination;
private boolean mFatalSigningError;
private FormText mDetailText;
/** The Apk Config map for the current project */
private Map<String, String> mApkConfig;
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
}
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() {
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();
mApkConfig = Sdk.getCurrent().getProjectApkConfigs(project);
String destination = ProjectHelper.loadStringProperty(project,
ExportWizard.PROPERTY_DESTINATION);
String filename = ProjectHelper.loadStringProperty(project,
ExportWizard.PROPERTY_FILENAME);
if (destination != null && filename != null) {
mDestination.setText(destination + File.separator + filename);
}
}
// 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 < 25) {
sb.append("<p>Make sure the certificate is valid for the planned lifetime of the product.</p>");
sb.append("<p>If the certificate expires, you will be forced to sign your application with a different one.</p>");
sb.append("<p>Applications cannot be upgraded if their certificate changes from one version to another, ");
sb.append("forcing a full uninstall/install, which will make the user lose his/her data.</p>");
sb.append("<p>Android Market currently requires certificates to be valid until 2033.</p>");
}
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 + 25 < 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("<p>Make sure the certificate is valid for the planned lifetime of the product.</p>");
sb.append("<p>If the certificate expires, you will be forced to sign your application with a different one.</p>");
sb.append("<p>Applications cannot be upgraded if their certificate changes from one version to another, ");
sb.append("forcing a full uninstall/install, which will make the user lose his/her data.</p>");
sb.append("<p>Android Market currently requires certificates to be valid until 2033.</p>");
}
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;
}
// display the list of files that will actually be created
Map<String, String[]> apkFileMap = getApkFileMap(file);
// display them
boolean fileExists = false;
StringBuilder sb = new StringBuilder(String.format(
"<p>This will create the following files:</p>"));
Set<Entry<String, String[]>> set = apkFileMap.entrySet();
for (Entry<String, String[]> entry : set) {
String[] apkArray = entry.getValue();
String filename = apkArray[ExportWizard.APK_FILE_DEST];
File f = new File(parentFolder, filename);
if (f.isFile()) {
fileExists = true;
sb.append(String.format("<li>%1$s (WARNING: already exists)</li>", filename));
} else if (f.isDirectory()) {
setErrorMessage(String.format("%1$s is a directory.", filename));
// reset canFinish in the wizard.
mWizard.resetDestination();
setPageComplete(false);
return;
} else {
sb.append(String.format("<li>%1$s</li>", filename));
}
}
mDestinationDetails = sb.toString();
// no error, set the destination in the wizard.
mWizard.setDestination(parentFolder, apkFileMap);
setPageComplete(true);
// However, we should also test if the file already exists.
if (fileExists) {
setMessage("A destination file already exists.", WARNING);
}
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();
}
/**
* Creates the list of destination filenames based on the content of the destination field
* and the list of APK configurations for the project.
*
* @param file File name from the destination field
* @return A list of destination filenames based <code>file</code> and the list of APK
* configurations for the project.
*/
private Map<String, String[]> getApkFileMap(File file) {
String filename = file.getName();
HashMap<String, String[]> map = new HashMap<String, String[]>();
// add the default APK filename
String[] apkArray = new String[ExportWizard.APK_COUNT];
apkArray[ExportWizard.APK_FILE_SOURCE] = ProjectHelper.getApkFilename(
mWizard.getProject(), null /*config*/);
apkArray[ExportWizard.APK_FILE_DEST] = filename;
map.put(null, apkArray);
// add the APKs for each APK configuration.
if (mApkConfig != null && mApkConfig.size() > 0) {
// remove the extension.
int index = filename.lastIndexOf('.');
String base = filename.substring(0, index);
String extension = filename.substring(index);
Set<Entry<String, String>> set = mApkConfig.entrySet();
for (Entry<String, String> entry : set) {
apkArray = new String[ExportWizard.APK_COUNT];
apkArray[ExportWizard.APK_FILE_SOURCE] = ProjectHelper.getApkFilename(
mWizard.getProject(), entry.getKey());
apkArray[ExportWizard.APK_FILE_DEST] = base + "-" + entry.getKey() + extension;
map.put(entry.getKey(), apkArray);
}
}
return map;
}
@Override
protected void onException(Throwable t) {
super.onException(t);
mKeyDetails = String.format("ERROR: %1$s", ExportWizard.getExceptionMessage(t));
}
}