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