blob: 15e9912cfb6d5c8418bd9d122a9807d952bf5cb6 [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.AdtConstants;
import com.android.ide.eclipse.adt.AdtPlugin;
import com.android.ide.eclipse.adt.internal.project.BaseProjectHelper;
import com.android.ide.eclipse.adt.internal.project.ProjectHelper;
import com.android.jarutils.KeystoreHelper;
import com.android.jarutils.SignedJarBuilder;
import com.android.jarutils.DebugKeyProvider.IKeyGenOutput;
import org.eclipse.core.resources.IFolder;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.resources.IncrementalProjectBuilder;
import org.eclipse.core.runtime.IAdaptable;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.jface.operation.IRunnableWithProgress;
import org.eclipse.jface.resource.ImageDescriptor;
import org.eclipse.jface.viewers.IStructuredSelection;
import org.eclipse.jface.wizard.Wizard;
import org.eclipse.jface.wizard.WizardPage;
import org.eclipse.swt.events.VerifyEvent;
import org.eclipse.swt.events.VerifyListener;
import org.eclipse.swt.widgets.Text;
import org.eclipse.ui.IExportWizard;
import org.eclipse.ui.IWorkbench;
import org.eclipse.ui.PlatformUI;
import java.io.BufferedReader;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintStream;
import java.lang.reflect.InvocationTargetException;
import java.security.KeyStore;
import java.security.PrivateKey;
import java.security.KeyStore.PrivateKeyEntry;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.Map.Entry;
/**
* Export wizard to export an apk signed with a release key/certificate.
*/
public final class ExportWizard extends Wizard implements IExportWizard {
private static final String PROJECT_LOGO_LARGE = "icons/android_large.png"; //$NON-NLS-1$
private static final String PAGE_PROJECT_CHECK = "Page_ProjectCheck"; //$NON-NLS-1$
private static final String PAGE_KEYSTORE_SELECTION = "Page_KeystoreSelection"; //$NON-NLS-1$
private static final String PAGE_KEY_CREATION = "Page_KeyCreation"; //$NON-NLS-1$
private static final String PAGE_KEY_SELECTION = "Page_KeySelection"; //$NON-NLS-1$
private static final String PAGE_KEY_CHECK = "Page_KeyCheck"; //$NON-NLS-1$
static final String PROPERTY_KEYSTORE = "keystore"; //$NON-NLS-1$
static final String PROPERTY_ALIAS = "alias"; //$NON-NLS-1$
static final String PROPERTY_DESTINATION = "destination"; //$NON-NLS-1$
static final String PROPERTY_FILENAME = "baseFilename"; //$NON-NLS-1$
static final int APK_FILE_SOURCE = 0;
static final int APK_FILE_DEST = 1;
static final int APK_COUNT = 2;
/**
* Base page class for the ExportWizard page. This class add the {@link #onShow()} callback.
*/
static abstract class ExportWizardPage extends WizardPage {
/** bit mask constant for project data change event */
protected static final int DATA_PROJECT = 0x001;
/** bit mask constant for keystore data change event */
protected static final int DATA_KEYSTORE = 0x002;
/** bit mask constant for key data change event */
protected static final int DATA_KEY = 0x004;
protected static final VerifyListener sPasswordVerifier = new VerifyListener() {
public void verifyText(VerifyEvent e) {
// verify the characters are valid for password.
int len = e.text.length();
// first limit to 127 characters max
if (len + ((Text)e.getSource()).getText().length() > 127) {
e.doit = false;
return;
}
// now only take non control characters
for (int i = 0 ; i < len ; i++) {
if (e.text.charAt(i) < 32) {
e.doit = false;
return;
}
}
}
};
/**
* Bit mask indicating what changed while the page was hidden.
* @see #DATA_PROJECT
* @see #DATA_KEYSTORE
* @see #DATA_KEY
*/
protected int mProjectDataChanged = 0;
ExportWizardPage(String name) {
super(name);
}
abstract void onShow();
@Override
public void setVisible(boolean visible) {
super.setVisible(visible);
if (visible) {
onShow();
mProjectDataChanged = 0;
}
}
final void projectDataChanged(int changeMask) {
mProjectDataChanged |= changeMask;
}
/**
* Calls {@link #setErrorMessage(String)} and {@link #setPageComplete(boolean)} based on a
* {@link Throwable} object.
*/
protected void onException(Throwable t) {
String message = getExceptionMessage(t);
setErrorMessage(message);
setPageComplete(false);
}
}
private ExportWizardPage mPages[] = new ExportWizardPage[5];
private IProject mProject;
private String mKeystore;
private String mKeystorePassword;
private boolean mKeystoreCreationMode;
private String mKeyAlias;
private String mKeyPassword;
private int mValidity;
private String mDName;
private PrivateKey mPrivateKey;
private X509Certificate mCertificate;
private File mDestinationParentFolder;
private ExportWizardPage mKeystoreSelectionPage;
private ExportWizardPage mKeyCreationPage;
private ExportWizardPage mKeySelectionPage;
private ExportWizardPage mKeyCheckPage;
private boolean mKeyCreationMode;
private List<String> mExistingAliases;
private Map<String, String[]> mApkMap;
public ExportWizard() {
setHelpAvailable(false); // TODO have help
setWindowTitle("Export Android Application");
setImageDescriptor();
}
@Override
public void addPages() {
addPage(mPages[0] = new ProjectCheckPage(this, PAGE_PROJECT_CHECK));
addPage(mKeystoreSelectionPage = mPages[1] = new KeystoreSelectionPage(this,
PAGE_KEYSTORE_SELECTION));
addPage(mKeyCreationPage = mPages[2] = new KeyCreationPage(this, PAGE_KEY_CREATION));
addPage(mKeySelectionPage = mPages[3] = new KeySelectionPage(this, PAGE_KEY_SELECTION));
addPage(mKeyCheckPage = mPages[4] = new KeyCheckPage(this, PAGE_KEY_CHECK));
}
@Override
public boolean performFinish() {
// save the properties
ProjectHelper.saveStringProperty(mProject, PROPERTY_KEYSTORE, mKeystore);
ProjectHelper.saveStringProperty(mProject, PROPERTY_ALIAS, mKeyAlias);
ProjectHelper.saveStringProperty(mProject, PROPERTY_DESTINATION,
mDestinationParentFolder.getAbsolutePath());
ProjectHelper.saveStringProperty(mProject, PROPERTY_FILENAME,
mApkMap.get(null)[APK_FILE_DEST]);
// run the export in an UI runnable.
IWorkbench workbench = PlatformUI.getWorkbench();
final boolean[] result = new boolean[1];
try {
workbench.getProgressService().busyCursorWhile(new IRunnableWithProgress() {
/**
* Run the export.
* @throws InvocationTargetException
* @throws InterruptedException
*/
public void run(IProgressMonitor monitor) throws InvocationTargetException,
InterruptedException {
try {
result[0] = doExport(monitor);
} finally {
monitor.done();
}
}
});
} catch (InvocationTargetException e) {
return false;
} catch (InterruptedException e) {
return false;
}
return result[0];
}
private boolean doExport(IProgressMonitor monitor) {
try {
// first we make sure the project is built
mProject.build(IncrementalProjectBuilder.INCREMENTAL_BUILD, monitor);
// if needed, create the keystore and/or key.
if (mKeystoreCreationMode || mKeyCreationMode) {
final ArrayList<String> output = new ArrayList<String>();
boolean createdStore = KeystoreHelper.createNewStore(
mKeystore,
null /*storeType*/,
mKeystorePassword,
mKeyAlias,
mKeyPassword,
mDName,
mValidity,
new IKeyGenOutput() {
public void err(String message) {
output.add(message);
}
public void out(String message) {
output.add(message);
}
});
if (createdStore == false) {
// keystore creation error!
displayError(output.toArray(new String[output.size()]));
return false;
}
// keystore is created, now load the private key and certificate.
KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
FileInputStream fis = new FileInputStream(mKeystore);
keyStore.load(fis, mKeystorePassword.toCharArray());
fis.close();
PrivateKeyEntry entry = (KeyStore.PrivateKeyEntry)keyStore.getEntry(
mKeyAlias, new KeyStore.PasswordProtection(mKeyPassword.toCharArray()));
if (entry != null) {
mPrivateKey = entry.getPrivateKey();
mCertificate = (X509Certificate)entry.getCertificate();
} else {
// this really shouldn't happen since we now let the user choose the key
// from a list read from the store.
displayError("Could not find key");
return false;
}
}
// check the private key/certificate again since it may have been created just above.
if (mPrivateKey != null && mCertificate != null) {
// get the output folder of the project to export.
// this is where we'll find the built apks to resign and export.
IFolder outputIFolder = BaseProjectHelper.getOutputFolder(mProject);
if (outputIFolder == null) {
return false;
}
String outputOsPath = outputIFolder.getLocation().toOSString();
// now generate the packages.
Set<Entry<String, String[]>> set = mApkMap.entrySet();
boolean runZipAlign = false;
String path = AdtPlugin.getOsAbsoluteZipAlign();
File zipalign = new File(path);
runZipAlign = zipalign.isFile();
for (Entry<String, String[]> entry : set) {
String[] defaultApk = entry.getValue();
String srcFilename = defaultApk[APK_FILE_SOURCE];
String destFilename = defaultApk[APK_FILE_DEST];
File destFile;
if (runZipAlign) {
destFile = File.createTempFile("android", ".apk");
} else {
destFile = new File(mDestinationParentFolder, destFilename);
}
FileOutputStream fos = new FileOutputStream(destFile);
SignedJarBuilder builder = new SignedJarBuilder(fos, mPrivateKey, mCertificate);
// get the input file.
FileInputStream fis = new FileInputStream(new File(outputOsPath, srcFilename));
// add the content of the source file to the output file, and sign it at
// the same time.
try {
builder.writeZip(fis, null /* filter */);
// close the builder: write the final signature files,
// and close the archive.
builder.close();
// now zipalign the result
if (runZipAlign) {
File realDestFile = new File(mDestinationParentFolder, destFilename);
String message = zipAlign(path, destFile, realDestFile);
if (message != null) {
displayError(message);
return false;
}
}
} finally {
try {
fis.close();
} finally {
fos.close();
}
}
}
// export success. In case we didn't run ZipAlign we display a warning
if (runZipAlign == false) {
AdtPlugin.displayWarning("Export Wizard",
"The zipalign tool was not found in the SDK.\n\n" +
"Please update to the latest SDK and re-export your application\n" +
"or run zipalign manually.\n\n" +
"Aligning applications allows Android to use application resources\n" +
"more efficiently.");
}
return true;
}
} catch (Throwable t) {
displayError(t);
}
return false;
}
@Override
public boolean canFinish() {
// check if we have the apk to resign, the destination location, and either
// a private key/certificate or the creation mode. In creation mode, unless
// all the key/keystore info is valid, the user cannot reach the last page, so there's
// no need to check them again here.
return mApkMap != null && mApkMap.size() > 0 &&
((mPrivateKey != null && mCertificate != null)
|| mKeystoreCreationMode || mKeyCreationMode) &&
mDestinationParentFolder != null;
}
/*
* (non-Javadoc)
* @see org.eclipse.ui.IWorkbenchWizard#init(org.eclipse.ui.IWorkbench, org.eclipse.jface.viewers.IStructuredSelection)
*/
public void init(IWorkbench workbench, IStructuredSelection selection) {
// get the project from the selection
Object selected = selection.getFirstElement();
if (selected instanceof IProject) {
mProject = (IProject)selected;
} else if (selected instanceof IAdaptable) {
IResource r = (IResource)((IAdaptable)selected).getAdapter(IResource.class);
if (r != null) {
mProject = r.getProject();
}
}
}
ExportWizardPage getKeystoreSelectionPage() {
return mKeystoreSelectionPage;
}
ExportWizardPage getKeyCreationPage() {
return mKeyCreationPage;
}
ExportWizardPage getKeySelectionPage() {
return mKeySelectionPage;
}
ExportWizardPage getKeyCheckPage() {
return mKeyCheckPage;
}
/**
* Returns an image descriptor for the wizard logo.
*/
private void setImageDescriptor() {
ImageDescriptor desc = AdtPlugin.getImageDescriptor(PROJECT_LOGO_LARGE);
setDefaultPageImageDescriptor(desc);
}
IProject getProject() {
return mProject;
}
void setProject(IProject project) {
mProject = project;
updatePageOnChange(ExportWizardPage.DATA_PROJECT);
}
void setKeystore(String path) {
mKeystore = path;
mPrivateKey = null;
mCertificate = null;
updatePageOnChange(ExportWizardPage.DATA_KEYSTORE);
}
String getKeystore() {
return mKeystore;
}
void setKeystoreCreationMode(boolean createStore) {
mKeystoreCreationMode = createStore;
updatePageOnChange(ExportWizardPage.DATA_KEYSTORE);
}
boolean getKeystoreCreationMode() {
return mKeystoreCreationMode;
}
void setKeystorePassword(String password) {
mKeystorePassword = password;
mPrivateKey = null;
mCertificate = null;
updatePageOnChange(ExportWizardPage.DATA_KEYSTORE);
}
String getKeystorePassword() {
return mKeystorePassword;
}
void setKeyCreationMode(boolean createKey) {
mKeyCreationMode = createKey;
updatePageOnChange(ExportWizardPage.DATA_KEY);
}
boolean getKeyCreationMode() {
return mKeyCreationMode;
}
void setExistingAliases(List<String> aliases) {
mExistingAliases = aliases;
}
List<String> getExistingAliases() {
return mExistingAliases;
}
void setKeyAlias(String name) {
mKeyAlias = name;
mPrivateKey = null;
mCertificate = null;
updatePageOnChange(ExportWizardPage.DATA_KEY);
}
String getKeyAlias() {
return mKeyAlias;
}
void setKeyPassword(String password) {
mKeyPassword = password;
mPrivateKey = null;
mCertificate = null;
updatePageOnChange(ExportWizardPage.DATA_KEY);
}
String getKeyPassword() {
return mKeyPassword;
}
void setValidity(int validity) {
mValidity = validity;
updatePageOnChange(ExportWizardPage.DATA_KEY);
}
int getValidity() {
return mValidity;
}
void setDName(String dName) {
mDName = dName;
updatePageOnChange(ExportWizardPage.DATA_KEY);
}
String getDName() {
return mDName;
}
void setSigningInfo(PrivateKey privateKey, X509Certificate certificate) {
mPrivateKey = privateKey;
mCertificate = certificate;
}
void setDestination(File parentFolder, Map<String, String[]> apkMap) {
mDestinationParentFolder = parentFolder;
mApkMap = apkMap;
}
void resetDestination() {
mDestinationParentFolder = null;
mApkMap = null;
}
void updatePageOnChange(int changeMask) {
for (ExportWizardPage page : mPages) {
page.projectDataChanged(changeMask);
}
}
private void displayError(String... messages) {
String message = null;
if (messages.length == 1) {
message = messages[0];
} else {
StringBuilder sb = new StringBuilder(messages[0]);
for (int i = 1; i < messages.length; i++) {
sb.append('\n');
sb.append(messages[i]);
}
message = sb.toString();
}
AdtPlugin.displayError("Export Wizard", message);
}
private void displayError(Throwable t) {
String message = getExceptionMessage(t);
displayError(message);
AdtPlugin.log(t, "Export Wizard Error");
}
/**
* Executes zipalign
* @param zipAlignPath location of the zipalign too
* @param source file to zipalign
* @param destination where to write the resulting file
* @return null if success, the error otherwise
* @throws IOException
*/
private String zipAlign(String zipAlignPath, File source, File destination) throws IOException {
// command line: zipaling -f 4 tmp destination
String[] command = new String[5];
command[0] = zipAlignPath;
command[1] = "-f"; //$NON-NLS-1$
command[2] = "4"; //$NON-NLS-1$
command[3] = source.getAbsolutePath();
command[4] = destination.getAbsolutePath();
Process process = Runtime.getRuntime().exec(command);
ArrayList<String> output = new ArrayList<String>();
try {
if (grabProcessOutput(process, output) != 0) {
// build a single message from the array list
StringBuilder sb = new StringBuilder("Error while running zipalign:");
for (String msg : output) {
sb.append('\n');
sb.append(msg);
}
return sb.toString();
}
} catch (InterruptedException e) {
// ?
}
return null;
}
/**
* Get the stderr output of a process and return when the process is done.
* @param process The process to get the ouput from
* @param results The array to store the stderr output
* @return the process return code.
* @throws InterruptedException
*/
private final int grabProcessOutput(final Process process,
final ArrayList<String> results)
throws InterruptedException {
// Due to the limited buffer size on windows for the standard io (stderr, stdout), we
// *need* to read both stdout and stderr all the time. If we don't and a process output
// a large amount, this could deadlock the process.
// read the lines as they come. if null is returned, it's
// because the process finished
new Thread("") { //$NON-NLS-1$
@Override
public void run() {
// create a buffer to read the stderr output
InputStreamReader is = new InputStreamReader(process.getErrorStream());
BufferedReader errReader = new BufferedReader(is);
try {
while (true) {
String line = errReader.readLine();
if (line != null) {
results.add(line);
} else {
break;
}
}
} catch (IOException e) {
// do nothing.
}
}
}.start();
new Thread("") { //$NON-NLS-1$
@Override
public void run() {
InputStreamReader is = new InputStreamReader(process.getInputStream());
BufferedReader outReader = new BufferedReader(is);
IProject project = getProject();
try {
while (true) {
String line = outReader.readLine();
if (line != null) {
AdtPlugin.printBuildToConsole(AdtConstants.BUILD_VERBOSE,
project, line);
} else {
break;
}
}
} catch (IOException e) {
// do nothing.
}
}
}.start();
// get the return code from the process
return process.waitFor();
}
/**
* Returns the {@link Throwable#getMessage()}. If the {@link Throwable#getMessage()} returns
* <code>null</code>, the method is called again on the cause of the Throwable object.
* <p/>If no Throwable in the chain has a valid message, the canonical name of the first
* exception is returned.
*/
static String getExceptionMessage(Throwable t) {
String message = t.getMessage();
if (message == null) {
// no error info? get the stack call to display it
// At least that'll give us a better bug report.
ByteArrayOutputStream baos = new ByteArrayOutputStream();
t.printStackTrace(new PrintStream(baos));
message = baos.toString();
}
return message;
}
}