aboutsummaryrefslogtreecommitdiff
path: root/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/export/ExportWizard.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/ExportWizard.java')
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/export/ExportWizard.java626
1 files changed, 626 insertions, 0 deletions
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/export/ExportWizard.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/export/ExportWizard.java
new file mode 100644
index 000000000..170da6d33
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/wizards/export/ExportWizard.java
@@ -0,0 +1,626 @@
+/*
+ * 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.annotations.Nullable;
+import com.android.ide.eclipse.adt.AdtPlugin;
+import com.android.ide.eclipse.adt.internal.sdk.ProjectState;
+import com.android.ide.eclipse.adt.internal.sdk.Sdk;
+import com.android.ide.eclipse.adt.internal.utils.FingerprintUtils;
+import com.android.ide.eclipse.adt.internal.preferences.AdtPrefs.BuildVerbosity;
+import com.android.ide.eclipse.adt.internal.project.ExportHelper;
+import com.android.ide.eclipse.adt.internal.project.ProjectHelper;
+import com.android.sdklib.BuildToolInfo;
+import com.android.sdklib.BuildToolInfo.PathId;
+import com.android.sdklib.internal.build.DebugKeyProvider.IKeyGenOutput;
+import com.android.sdklib.internal.build.KeystoreHelper;
+import com.android.utils.GrabProcessOutput;
+import com.android.utils.GrabProcessOutput.IProcessOutput;
+import com.android.utils.GrabProcessOutput.Wait;
+
+import org.eclipse.core.resources.IProject;
+import org.eclipse.core.resources.IResource;
+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.ByteArrayOutputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.PrintStream;
+import java.lang.reflect.InvocationTargetException;
+import java.security.KeyStore;
+import java.security.KeyStore.PrivateKeyEntry;
+import java.security.PrivateKey;
+import java.security.cert.X509Certificate;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * 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-64.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 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() {
+ @Override
+ 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 mDestinationFile;
+
+ private ExportWizardPage mKeystoreSelectionPage;
+ private ExportWizardPage mKeyCreationPage;
+ private ExportWizardPage mKeySelectionPage;
+ private ExportWizardPage mKeyCheckPage;
+
+ private boolean mKeyCreationMode;
+
+ private List<String> mExistingAliases;
+
+ 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,
+ mDestinationFile.getAbsolutePath());
+
+ // 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
+ */
+ @Override
+ 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 {
+ // 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() {
+ @Override
+ public void err(String message) {
+ output.add(message);
+ }
+ @Override
+ 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();
+
+ AdtPlugin.printToConsole(mProject,
+ String.format("New keystore %s has been created.",
+ mDestinationFile.getAbsolutePath()),
+ "Certificate fingerprints:",
+ String.format(" MD5 : %s", getCertMd5Fingerprint()),
+ String.format(" SHA1: %s", getCertSha1Fingerprint()));
+
+ } 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) {
+ // check whether we can run zipalign.
+ boolean runZipAlign = false;
+
+ ProjectState projectState = Sdk.getProjectState(mProject);
+ BuildToolInfo buildToolInfo = ExportHelper.getBuildTools(projectState);
+
+ String zipAlignPath = buildToolInfo.getPath(PathId.ZIP_ALIGN);
+ runZipAlign = zipAlignPath != null && new File(zipAlignPath).isFile();
+
+ File apkExportFile = mDestinationFile;
+ if (runZipAlign) {
+ // create a temp file for the original export.
+ apkExportFile = File.createTempFile("androidExport_", ".apk");
+ }
+
+ // export the signed apk.
+ ExportHelper.exportReleaseApk(mProject, apkExportFile,
+ mPrivateKey, mCertificate, monitor);
+
+ // align if we can
+ if (runZipAlign) {
+ String message = zipAlign(zipAlignPath, apkExportFile, mDestinationFile);
+ if (message != null) {
+ displayError(message);
+ return false;
+ }
+ } else {
+ 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 ((mPrivateKey != null && mCertificate != null)
+ || mKeystoreCreationMode || mKeyCreationMode) &&
+ mDestinationFile != null;
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see org.eclipse.ui.IWorkbenchWizard#init(org.eclipse.ui.IWorkbench,
+ * org.eclipse.jface.viewers.IStructuredSelection)
+ */
+ @Override
+ 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;
+ }
+
+ String getCertSha1Fingerprint() {
+ return FingerprintUtils.getFingerprint(mCertificate, "SHA1");
+ }
+
+ String getCertMd5Fingerprint() {
+ return FingerprintUtils.getFingerprint(mCertificate, "MD5");
+ }
+
+ void setSigningInfo(PrivateKey privateKey, X509Certificate certificate) {
+ mPrivateKey = privateKey;
+ mCertificate = certificate;
+ }
+
+ void setDestination(File destinationFile) {
+ mDestinationFile = destinationFile;
+ }
+
+ void resetDestination() {
+ mDestinationFile = 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);
+ final ArrayList<String> output = new ArrayList<String>();
+ try {
+ final IProject project = getProject();
+
+ int status = GrabProcessOutput.grabProcessOutput(
+ process,
+ Wait.WAIT_FOR_READERS,
+ new IProcessOutput() {
+ @Override
+ public void out(@Nullable String line) {
+ if (line != null) {
+ AdtPlugin.printBuildToConsole(BuildVerbosity.VERBOSE,
+ project, line);
+ }
+ }
+
+ @Override
+ public void err(@Nullable String line) {
+ if (line != null) {
+ output.add(line);
+ }
+ }
+ });
+
+ if (status != 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;
+ }
+
+ /**
+ * 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;
+ }
+}