diff --git a/java/idea-ui/src/com/intellij/openapi/roots/ui/configuration/artifacts/sourceItems/FacetBasedPackagingSourceItemsProvider.java b/java/idea-ui/src/com/intellij/openapi/roots/ui/configuration/artifacts/sourceItems/FacetBasedPackagingSourceItemsProvider.java index c278f5499fe3..b5535a2db956 100644 --- a/java/idea-ui/src/com/intellij/openapi/roots/ui/configuration/artifacts/sourceItems/FacetBasedPackagingSourceItemsProvider.java +++ b/java/idea-ui/src/com/intellij/openapi/roots/ui/configuration/artifacts/sourceItems/FacetBasedPackagingSourceItemsProvider.java @@ -77,7 +77,7 @@ public abstract class FacetBasedPackagingSourceItemsProvider createElement(ArtifactEditorContext context, F facet); - private static class FacetBasedSourceItem extends PackagingSourceItem { + protected static class FacetBasedSourceItem extends PackagingSourceItem { private final FacetBasedPackagingSourceItemsProvider myProvider; private final F myFacet; diff --git a/plugins/android/common/src/org/jetbrains/android/util/AndroidCommonUtils.java b/plugins/android/common/src/org/jetbrains/android/util/AndroidCommonUtils.java index 6c90602ad3d3..df69b76e40f6 100644 --- a/plugins/android/common/src/org/jetbrains/android/util/AndroidCommonUtils.java +++ b/plugins/android/common/src/org/jetbrains/android/util/AndroidCommonUtils.java @@ -15,6 +15,7 @@ */ package org.jetbrains.android.util; +import com.android.jarutils.SignedJarBuilder; import com.android.resources.ResourceFolderType; import com.android.resources.ResourceType; import com.android.sdklib.IAndroidTarget; @@ -38,6 +39,9 @@ import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.io.*; +import java.security.GeneralSecurityException; +import java.security.PrivateKey; +import java.security.cert.X509Certificate; import java.util.*; import java.util.jar.JarEntry; import java.util.jar.JarOutputStream; @@ -90,6 +94,8 @@ public class AndroidCommonUtils { @NonNls public static final String PACKAGE_MANIFEST_ATTRIBUTE = "package"; + @NonNls public static final String ANDROID_FINAL_PACKAGE_FOR_ARTIFACT_SUFFIX = ".afp"; + private AndroidCommonUtils() { } @@ -529,4 +535,32 @@ public class AndroidCommonUtils { final String b = path.substring(dot); return a + suffix + b; } + + public static void signApk(@NotNull File srcApk, + @NotNull File destFile, + @NotNull PrivateKey privateKey, + @NotNull X509Certificate certificate) + throws IOException, GeneralSecurityException { + FileOutputStream fos = new FileOutputStream(destFile); + SignedJarBuilder builder = new SafeSignedJarBuilder(fos, privateKey, certificate, destFile.getPath()); + FileInputStream fis = new FileInputStream(srcApk); + try { + builder.writeZip(fis, null); + builder.close(); + } + finally { + try { + fis.close(); + } + catch (IOException ignored) { + } + finally { + try { + fos.close(); + } + catch (IOException ignored) { + } + } + } + } } diff --git a/plugins/android/src/META-INF/plugin.xml b/plugins/android/src/META-INF/plugin.xml index 23eb2b596175..3315888cb0df 100644 --- a/plugins/android/src/META-INF/plugin.xml +++ b/plugins/android/src/META-INF/plugin.xml @@ -219,6 +219,11 @@ + + + + + diff --git a/plugins/android/src/org/jetbrains/android/compiler/AndroidCompileUtil.java b/plugins/android/src/org/jetbrains/android/compiler/AndroidCompileUtil.java index 10551667f969..0875a4fb4be9 100644 --- a/plugins/android/src/org/jetbrains/android/compiler/AndroidCompileUtil.java +++ b/plugins/android/src/org/jetbrains/android/compiler/AndroidCompileUtil.java @@ -44,6 +44,9 @@ import com.intellij.openapi.util.Key; import com.intellij.openapi.util.Pair; import com.intellij.openapi.util.io.FileUtil; import com.intellij.openapi.vfs.*; +import com.intellij.packaging.artifacts.Artifact; +import com.intellij.packaging.artifacts.ArtifactProperties; +import com.intellij.packaging.impl.compiler.ArtifactCompileScope; import com.intellij.psi.JavaPsiFacade; import com.intellij.psi.PsiClass; import com.intellij.psi.PsiFile; @@ -52,6 +55,9 @@ import com.intellij.psi.search.GlobalSearchScope; import com.intellij.util.ArrayUtil; import com.intellij.util.containers.HashMap; import com.intellij.util.containers.HashSet; +import org.jetbrains.android.compiler.artifact.AndroidApplicationArtifactProperties; +import org.jetbrains.android.compiler.artifact.AndroidArtifactPropertiesProvider; +import org.jetbrains.android.compiler.artifact.AndroidArtifactSigningMode; import org.jetbrains.android.dom.manifest.Manifest; import org.jetbrains.android.dom.resources.Attr; import org.jetbrains.android.dom.resources.DeclareStyleable; @@ -86,6 +92,7 @@ public class AndroidCompileUtil { @NonNls public static final String PROGUARD_CFG_FILE_NAME = "proguard-project.txt"; @NonNls public static final String OLD_PROGUARD_CFG_FILE_NAME = "proguard.cfg"; + public static final String UNSIGNED_SUFFIX = ".unsigned"; private AndroidCompileUtil() { } @@ -131,7 +138,7 @@ public class AndroidCompileUtil { return null; } - static void addMessages(final CompileContext context, final Map> messages, Module module) { + public static void addMessages(final CompileContext context, final Map> messages, @Nullable Module module) { addMessages(context, messages, null, module); } @@ -150,7 +157,7 @@ public class AndroidCompileUtil { static void addMessages(final CompileContext context, final Map> messages, @Nullable final Map presentableFilesMap, - final Module module) { + @Nullable final Module module) { ApplicationManager.getApplication().runReadAction(new Runnable() { public void run() { if (context.getProject().isDisposed()) return; @@ -167,7 +174,7 @@ public class AndroidCompileUtil { line = Integer.parseInt(matcher.group(2)); } } - context.addMessage(category, '[' + module.getName() + "] " + message, url, line, -1); + context.addMessage(category, (module != null ? '[' + module.getName() + "] " : "") + message, url, line, -1); } } } @@ -619,7 +626,22 @@ public class AndroidCompileUtil { public static boolean isReleaseBuild(@NotNull CompileContext context) { final Boolean value = context.getCompileScope().getUserData(RELEASE_BUILD_KEY); - return value != null && value.booleanValue(); + if (value != null && value.booleanValue()) { + return true; + } + final Project project = context.getProject(); + final Set artifacts = ArtifactCompileScope.getArtifactsToBuild(project, context.getCompileScope(), false); + + if (artifacts != null) { + for (Artifact artifact : artifacts) { + final ArtifactProperties properties = artifact.getProperties(AndroidArtifactPropertiesProvider.getInstance()); + if (properties instanceof AndroidApplicationArtifactProperties && + ((AndroidApplicationArtifactProperties)properties).getSigningMode() != AndroidArtifactSigningMode.DEBUG) { + return true; + } + } + } + return false; } public static void setReleaseBuild(@NotNull CompileScope compileScope) { @@ -668,7 +690,10 @@ public class AndroidCompileUtil { } } - private static void initializeGenSourceRoot(@NotNull Module module, @Nullable String sourceRootPath, boolean createIfNotExist, boolean exclude) { + private static void initializeGenSourceRoot(@NotNull Module module, + @Nullable String sourceRootPath, + boolean createIfNotExist, + boolean exclude) { if (sourceRootPath == null) { return; } @@ -716,31 +741,31 @@ public class AndroidCompileUtil { final Resources resources = pair.getFirst(); waitForSmartMode(project); - ApplicationManager.getApplication().runReadAction(new Runnable() { - @Override - public void run() { - if (!resources.isValid() || facet.getModule().isDisposed() || project.isDisposed()) { - return; - } + ApplicationManager.getApplication().runReadAction(new Runnable() { + @Override + public void run() { + if (!resources.isValid() || facet.getModule().isDisposed() || project.isDisposed()) { + return; + } - for (final Attr attr : resources.getAttrs()) { - final String name = attr.getName().getValue(); + for (final Attr attr : resources.getAttrs()) { + final String name = attr.getName().getValue(); - if (name != null) { - resourceSet.add(new ResourceEntry(ResourceType.ATTR.getName(), name)); - } - } - - for (final DeclareStyleable styleable : resources.getDeclareStyleables()) { - final String name = styleable.getName().getValue(); - - if (name != null) { - resourceSet.add(new ResourceEntry(ResourceType.DECLARE_STYLEABLE.getName(), name)); - } + if (name != null) { + resourceSet.add(new ResourceEntry(ResourceType.ATTR.getName(), name)); } } - }); - } + + for (final DeclareStyleable styleable : resources.getDeclareStyleables()) { + final String name = styleable.getName().getValue(); + + if (name != null) { + resourceSet.add(new ResourceEntry(ResourceType.DECLARE_STYLEABLE.getName(), name)); + } + } + } + }); + } waitForSmartMode(project); @@ -929,4 +954,22 @@ public class AndroidCompileUtil { } return false; } + + @Nullable + public static String getUnsignedApkPath(@NotNull AndroidFacet facet) { + final String apkPath = AndroidRootUtil.getApkPath(facet); + return apkPath != null ? AndroidCommonUtils.addSuffixToFileName(apkPath, UNSIGNED_SUFFIX) : null; + } + + @Nullable + public static T handleExceptionError(@NotNull CompileContext context, + @NotNull String messagePrefix, + @NotNull Exception e) { + reportException(context, messagePrefix, e); + return null; + } + + public static void reportException(@NotNull CompileContext context, @NotNull String messagePrefix, @NotNull Exception e) { + context.addMessage(CompilerMessageCategory.ERROR, messagePrefix + e.getClass().getSimpleName() + ": " + e.getMessage(), null, -1, -1); + } } \ No newline at end of file diff --git a/plugins/android/src/org/jetbrains/android/compiler/AndroidPackagingCompiler.java b/plugins/android/src/org/jetbrains/android/compiler/AndroidPackagingCompiler.java index 3cf23e95fcd2..3bc201520429 100644 --- a/plugins/android/src/org/jetbrains/android/compiler/AndroidPackagingCompiler.java +++ b/plugins/android/src/org/jetbrains/android/compiler/AndroidPackagingCompiler.java @@ -16,6 +16,7 @@ package org.jetbrains.android.compiler; import com.intellij.compiler.CompilerIOUtil; +import com.intellij.compiler.impl.CompilerUtil; import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.compiler.*; import com.intellij.openapi.diagnostic.Logger; @@ -27,6 +28,7 @@ import com.intellij.openapi.roots.ModuleOrderEntry; import com.intellij.openapi.roots.ModuleRootManager; import com.intellij.openapi.roots.OrderEntry; import com.intellij.openapi.util.io.FileUtil; +import com.intellij.openapi.vfs.JarFileSystem; import com.intellij.openapi.vfs.VfsUtil; import com.intellij.openapi.vfs.VirtualFile; import com.intellij.util.containers.HashSet; @@ -54,8 +56,6 @@ import java.util.*; public class AndroidPackagingCompiler implements PackagingCompiler { private static final Logger LOG = Logger.getInstance("#org.jetbrains.android.compiler.AndroidPackagingCompiler"); - public static final String UNSIGNED_SUFFIX = ".unsigned"; - public void processOutdatedItem(CompileContext context, String url, @Nullable ValidityState state) { } @@ -235,7 +235,7 @@ public class AndroidPackagingCompiler implements PackagingCompiler { : item.getResPackagePath(); final String finalPath = unsigned - ? AndroidCommonUtils.addSuffixToFileName(item.getFinalPath(), UNSIGNED_SUFFIX) + ? AndroidCommonUtils.addSuffixToFileName(item.getFinalPath(), AndroidCompileUtil.UNSIGNED_SUFFIX) : item.getFinalPath(); final String[] sourceRoots = AndroidCompileUtil.toOsPaths(item.getSourceRoots()); @@ -247,6 +247,18 @@ public class AndroidPackagingCompiler implements PackagingCompiler { item.getAdditionalNativeLibs(), finalPath, unsigned, item.mySdkPath, item.getCustomKeystorePath(), new ExcludedSourcesFilter(project))); + if (messages.get(CompilerMessageCategory.ERROR).size() == 0) { + if (item.myReleaseBuild == unsigned) { + final File dst = new File( + AndroidCommonUtils.addSuffixToFileName(item.getFinalPath(), AndroidCommonUtils.ANDROID_FINAL_PACKAGE_FOR_ARTIFACT_SUFFIX)); + FileUtil.copy(new File(finalPath), dst); + CompilerUtil.refreshIOFile(dst); + final VirtualFile jar = JarFileSystem.getInstance().refreshAndFindFileByPath(dst.getPath() + "!/"); + if (jar != null) { + jar.refresh(false, true); + } + } + } AndroidCompileUtil.addMessages(context, messages, item.myModule); } catch (final IOException e) { diff --git a/plugins/android/src/org/jetbrains/android/compiler/AndroidPrecompileTask.java b/plugins/android/src/org/jetbrains/android/compiler/AndroidPrecompileTask.java index e4d6b8b2295f..3b952b9eb0c9 100644 --- a/plugins/android/src/org/jetbrains/android/compiler/AndroidPrecompileTask.java +++ b/plugins/android/src/org/jetbrains/android/compiler/AndroidPrecompileTask.java @@ -17,6 +17,8 @@ package org.jetbrains.android.compiler; import com.intellij.compiler.CompilerConfiguration; import com.intellij.compiler.CompilerConfigurationImpl; +import com.intellij.compiler.options.CompileStepBeforeRun; +import com.intellij.facet.ProjectFacetManager; import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.application.ModalityState; import com.intellij.openapi.compiler.*; @@ -35,7 +37,13 @@ import com.intellij.openapi.roots.OrderEntry; import com.intellij.openapi.util.io.FileUtil; import com.intellij.openapi.vfs.LocalFileSystem; import com.intellij.openapi.vfs.VirtualFile; +import com.intellij.packaging.artifacts.Artifact; +import com.intellij.packaging.artifacts.ArtifactProperties; +import com.intellij.packaging.impl.compiler.ArtifactCompileScope; import com.intellij.util.containers.hash.HashSet; +import org.jetbrains.android.compiler.artifact.AndroidApplicationArtifactProperties; +import org.jetbrains.android.compiler.artifact.AndroidArtifactPropertiesProvider; +import org.jetbrains.android.compiler.artifact.AndroidArtifactSigningMode; import org.jetbrains.android.facet.AndroidFacet; import org.jetbrains.android.facet.AndroidRootUtil; import org.jetbrains.android.maven.AndroidMavenUtil; @@ -55,10 +63,13 @@ public class AndroidPrecompileTask implements CompileTask { @Override public boolean execute(CompileContext context) { + if (!checkArtifacts(context)) { + return false; + } checkAndroidDependencies(context); final Project project = context.getProject(); - + ExcludedEntriesConfiguration configuration = ((CompilerConfigurationImpl)CompilerConfiguration.getInstance(project)).getExcludedEntriesConfiguration(); @@ -107,6 +118,62 @@ public class AndroidPrecompileTask implements CompileTask { return true; } + private static boolean checkArtifacts(@NotNull CompileContext context) { + final Project project = context.getProject(); + if (!ProjectFacetManager.getInstance(project).hasFacets(AndroidFacet.ID)) { + return true; + } + + final Set artifacts = ArtifactCompileScope.getArtifactsToBuild(project, context.getCompileScope(), false); + if (artifacts == null) { + return true; + } + final Set debugArtifacts = new HashSet(); + final Set releaseArtifacts = new HashSet(); + + for (final Artifact artifact : artifacts) { + final ArtifactProperties properties = artifact.getProperties(AndroidArtifactPropertiesProvider.getInstance()); + if (properties instanceof AndroidApplicationArtifactProperties) { + final AndroidArtifactSigningMode mode = ((AndroidApplicationArtifactProperties)properties).getSigningMode(); + if (mode == AndroidArtifactSigningMode.DEBUG) { + debugArtifacts.add(artifact); + } + else { + releaseArtifacts.add(artifact); + } + } + } + boolean success = true; + + if (debugArtifacts.size() > 0 && releaseArtifacts.size() > 0) { + final String message = "Cannot build debug and release Android artifacts in the same session\n" + + "Debug artifacts: " + toString(debugArtifacts) + "\n" + + "Release artifacts: " + toString(releaseArtifacts); + context.addMessage(CompilerMessageCategory.ERROR, message, null, -1, -1); + success = false; + } + + if (releaseArtifacts.size() > 0 && + CompileStepBeforeRun.getRunConfiguration(context) != null) { + final String message = "Cannot build release Android artifacts in the 'make before run' session\n" + + "Release artifacts: " + toString(releaseArtifacts); + context.addMessage(CompilerMessageCategory.ERROR, message, null, -1, -1); + success = false; + } + return success; + } + + private static String toString(Collection artifacts) { + final StringBuilder result = new StringBuilder(); + for (Artifact artifact : artifacts) { + if (result.length() > 0) { + result.append(", "); + } + result.append(artifact.getName()); + } + return result.toString(); + } + private static void checkAndroidDependencies(@NotNull CompileContext context) { for (Module module : context.getCompileScope().getAffectedModules()) { final AndroidFacet facet = AndroidFacet.getInstance(module); @@ -142,7 +209,7 @@ public class AndroidPrecompileTask implements CompileTask { } } } - + private static void clearResCache(@NotNull AndroidFacet facet, @NotNull CompileContext context) { final Module module = facet.getModule(); diff --git a/plugins/android/src/org/jetbrains/android/compiler/artifact/AndroidApplicationArtifactProperties.java b/plugins/android/src/org/jetbrains/android/compiler/artifact/AndroidApplicationArtifactProperties.java new file mode 100644 index 000000000000..c7ee3c802239 --- /dev/null +++ b/plugins/android/src/org/jetbrains/android/compiler/artifact/AndroidApplicationArtifactProperties.java @@ -0,0 +1,250 @@ +package org.jetbrains.android.compiler.artifact; + +import com.intellij.openapi.compiler.CompileContext; +import com.intellij.openapi.compiler.CompilerMessageCategory; +import com.intellij.openapi.diagnostic.Logger; +import com.intellij.openapi.util.Pair; +import com.intellij.openapi.util.io.FileUtil; +import com.intellij.openapi.vfs.VfsUtilCore; +import com.intellij.packaging.artifacts.Artifact; +import com.intellij.packaging.artifacts.ArtifactProperties; +import com.intellij.packaging.ui.ArtifactEditorContext; +import com.intellij.packaging.ui.ArtifactPropertiesEditor; +import com.intellij.util.xmlb.XmlSerializerUtil; +import com.intellij.util.xmlb.annotations.Transient; +import org.apache.commons.codec.binary.Base64; +import org.jetbrains.android.compiler.AndroidCompileUtil; +import org.jetbrains.android.util.AndroidBundle; +import org.jetbrains.android.util.AndroidCommonUtils; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.io.*; +import java.security.*; +import java.security.cert.Certificate; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; + +/** + * @author Eugene.Kudelevsky + */ +@SuppressWarnings("UnusedDeclaration") +public class AndroidApplicationArtifactProperties extends ArtifactProperties { + private static final Logger LOG = Logger.getInstance("#org.jetbrains.android.compiler.artifact.AndroidApplicationArtifactProperties"); + + private AndroidArtifactSigningMode mySigningMode = AndroidArtifactSigningMode.RELEASE_UNSIGNED; + private String myKeyStoreUrl = ""; + private String myKeyStorePassword = ""; + private String myKeyAlias = ""; + private String myKeyPassword = ""; + + @Override + public void onBuildFinished(@NotNull Artifact artifact, @NotNull CompileContext context) { + if (!(artifact.getArtifactType() instanceof AndroidApplicationArtifactType) || + mySigningMode != AndroidArtifactSigningMode.RELEASE_SIGNED) { + return; + } + final String errorPrefix = "[Artifact '" + artifact.getName() + "'] "; + final Pair pair = getPrivateKeyAndCertificate(context, errorPrefix); + if (pair == null) { + return; + } + + final String artifactFilePath = artifact.getOutputFilePath(); + final String prefix = "Cannot sign artifact " + artifact.getName() + ": "; + + if (artifactFilePath == null) { + context.addMessage(CompilerMessageCategory.ERROR, prefix + "output path is not specified", null, -1, -1); + return; + } + + final File artifactFile = new File(artifactFilePath); + if (!artifactFile.exists()) { + context.addMessage(CompilerMessageCategory.ERROR, prefix + "file " + artifactFilePath + " hasn't been generated", null, -1, -1); + return; + } + + File tmpDir = null; + try { + tmpDir = FileUtil.createTempDirectory("android_artifact", "tmp"); + final File tmpArtifact = new File(tmpDir, "tmpArtifact.apk"); + FileUtil.copy(artifactFile, tmpArtifact); + + if (!FileUtil.delete(artifactFile)) { + context.addMessage(CompilerMessageCategory.ERROR, "Cannot delete file " + artifactFile.getPath(), null, -1, -1); + return; + } + AndroidCommonUtils.signApk(tmpArtifact, artifactFile, pair.getFirst(), pair.getSecond()); + } + catch (IOException e) { + context.addMessage(CompilerMessageCategory.ERROR, prefix + "I/O error: " + e.getMessage(), null, -1, -1); + } + catch (GeneralSecurityException e) { + AndroidCompileUtil.reportException(context, prefix, e); + } + finally { + if (tmpDir != null) { + FileUtil.delete(tmpDir); + } + } + } + + @Override + public ArtifactPropertiesEditor createEditor(@NotNull ArtifactEditorContext context) { + return new AndroidArtifactPropertiesEditor(this, context.getProject()); + } + + @Override + public AndroidApplicationArtifactProperties getState() { + return this; + } + + @Override + public void loadState(AndroidApplicationArtifactProperties state) { + XmlSerializerUtil.copyBean(state, this); + } + + @Nullable + private Pair getPrivateKeyAndCertificate(@NotNull CompileContext context, @NotNull String errorPrefix) { + final String keyStoreFilePath = myKeyStoreUrl != null ? VfsUtilCore.urlToPath(myKeyStoreUrl) : ""; + + if (keyStoreFilePath.length() == 0) { + context.addMessage(CompilerMessageCategory.ERROR, errorPrefix + "Key store file is not specified", null, -1, -1); + return null; + } + if (myKeyStorePassword == null || myKeyStorePassword.length() == 0) { + context.addMessage(CompilerMessageCategory.ERROR, errorPrefix + "Key store password is not specified", null, -1, -1); + return null; + } + if (myKeyAlias == null || myKeyAlias.length() == 0) { + context.addMessage(CompilerMessageCategory.ERROR, errorPrefix + "Key alias is not specified", null, -1, -1); + return null; + } + if (myKeyPassword == null || myKeyPassword.length() == 0) { + context.addMessage(CompilerMessageCategory.ERROR, errorPrefix + "Key password is not specified", null, -1, -1); + return null; + } + final File keyStoreFile = new File(keyStoreFilePath); + final String keystorePasswordStr = getPlainKeystorePassword(); + final char[] keystorePassword = keystorePasswordStr.toCharArray(); + + final String keyPasswordStr = getPlainKeyPassword(); + final char[] keyPassword = keyPasswordStr.toCharArray(); + + final KeyStore keyStore; + InputStream is = null; + try { + //noinspection IOResourceOpenedButNotSafelyClosed + is = new FileInputStream(keyStoreFile); + keyStore = KeyStore.getInstance(KeyStore.getDefaultType()); + keyStore.load(is, keystorePassword); + + final KeyStore.PrivateKeyEntry entry = + (KeyStore.PrivateKeyEntry)keyStore.getEntry(myKeyAlias, new KeyStore.PasswordProtection(keyPassword)); + if (entry == null) { + context.addMessage(CompilerMessageCategory.ERROR, + errorPrefix + AndroidBundle.message("android.extract.package.cannot.find.key.error", myKeyAlias), null, -1, -1); + return null; + } + + final PrivateKey privateKey = entry.getPrivateKey(); + final Certificate certificate = entry.getCertificate(); + if (privateKey == null || certificate == null) { + context.addMessage(CompilerMessageCategory.ERROR, + errorPrefix + AndroidBundle.message("android.extract.package.cannot.find.key.error", myKeyAlias), null, -1, -1); + return null; + } + return Pair.create(privateKey, (X509Certificate)certificate); + } + catch (FileNotFoundException e) { + return AndroidCompileUtil.handleExceptionError(context, errorPrefix, e); + } + catch (KeyStoreException e) { + return AndroidCompileUtil.handleExceptionError(context, errorPrefix, e); + } + catch (CertificateException e) { + return AndroidCompileUtil.handleExceptionError(context, errorPrefix, e); + } + catch (NoSuchAlgorithmException e) { + return AndroidCompileUtil.handleExceptionError(context, errorPrefix, e); + } + catch (IOException e) { + return AndroidCompileUtil.handleExceptionError(context, errorPrefix, e); + } + catch (UnrecoverableEntryException e) { + return AndroidCompileUtil.handleExceptionError(context, errorPrefix, e); + } + finally { + if (is != null) { + try { + is.close(); + } + catch (IOException e) { + LOG.info(e); + } + } + } + } + + public AndroidArtifactSigningMode getSigningMode() { + return mySigningMode; + } + + public void setSigningMode(AndroidArtifactSigningMode signingMode) { + mySigningMode = signingMode; + } + + public String getKeyStoreUrl() { + return myKeyStoreUrl; + } + + public String getKeyStorePassword() { + return myKeyStorePassword; + } + + public String getKeyAlias() { + return myKeyAlias; + } + + public String getKeyPassword() { + return myKeyPassword; + } + + public void setKeyStoreUrl(String keyStoreUrl) { + myKeyStoreUrl = keyStoreUrl; + } + + public void setKeyStorePassword(String keyStorePassword) { + myKeyStorePassword = keyStorePassword; + } + + public void setKeyAlias(String keyAlias) { + myKeyAlias = keyAlias; + } + + public void setKeyPassword(String keyPassword) { + myKeyPassword = keyPassword; + } + + @Transient + @NotNull + public String getPlainKeystorePassword() { + return new String(new Base64().decode(myKeyStorePassword.getBytes())); + } + + @Transient + public void setPlainKeystorePassword(@NotNull String password) { + myKeyStorePassword = new String(new Base64().encode(password.getBytes())); + } + + @Transient + @NotNull + public String getPlainKeyPassword() { + return new String(new Base64().decode(myKeyPassword.getBytes())); + } + + @Transient + public void setPlainKeyPassword(@NotNull String password) { + myKeyPassword = new String(new Base64().encode(password.getBytes())); + } +} diff --git a/plugins/android/src/org/jetbrains/android/compiler/artifact/AndroidApplicationArtifactType.java b/plugins/android/src/org/jetbrains/android/compiler/artifact/AndroidApplicationArtifactType.java new file mode 100644 index 000000000000..ea268a3fac13 --- /dev/null +++ b/plugins/android/src/org/jetbrains/android/compiler/artifact/AndroidApplicationArtifactType.java @@ -0,0 +1,95 @@ +package org.jetbrains.android.compiler.artifact; + +import com.intellij.openapi.module.Module; +import com.intellij.openapi.roots.ui.configuration.artifacts.sourceItems.LibrarySourceItem; +import com.intellij.openapi.roots.ui.configuration.artifacts.sourceItems.ModuleOutputSourceItem; +import com.intellij.packaging.artifacts.ArtifactTemplate; +import com.intellij.packaging.artifacts.ArtifactType; +import com.intellij.packaging.elements.CompositePackagingElement; +import com.intellij.packaging.elements.PackagingElementFactory; +import com.intellij.packaging.elements.PackagingElementOutputKind; +import com.intellij.packaging.elements.PackagingElementResolvingContext; +import com.intellij.packaging.impl.artifacts.ArtifactUtil; +import com.intellij.packaging.impl.artifacts.PlainArtifactType; +import com.intellij.packaging.ui.PackagingSourceItem; +import org.jetbrains.android.facet.AndroidFacet; +import org.jetbrains.annotations.NotNull; + +import javax.swing.*; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * @author Eugene.Kudelevsky + */ +public class AndroidApplicationArtifactType extends ArtifactType { + public AndroidApplicationArtifactType() { + super("apk", "Android Application"); + } + + @NotNull + @Override + public Icon getIcon() { + return PlainArtifactType.ARTIFACT_ICON; + } + + @Override + public String getDefaultPathFor(@NotNull PackagingElementOutputKind kind) { + return "/"; + } + + @Override + public boolean isSuitableItem(@NotNull PackagingSourceItem item) { + return !(item instanceof ModuleOutputSourceItem || item instanceof LibrarySourceItem); + } + + @NotNull + @Override + public CompositePackagingElement createRootElement(@NotNull String artifactName) { + return PackagingElementFactory.getInstance().createArchive(ArtifactUtil.suggestArtifactFileName(artifactName) + ".apk"); + } + + + @NotNull + @Override + public List getNewArtifactTemplates(@NotNull PackagingElementResolvingContext context) { + return Collections.singletonList(new MyTemplate(context)); + } + + private class MyTemplate extends ArtifactTemplate { + protected PackagingElementResolvingContext myContext; + + public MyTemplate(@NotNull PackagingElementResolvingContext context) { + myContext = context; + } + + @Override + public String getPresentableName() { + return "From module..."; + } + + @Override + public NewArtifactConfiguration createArtifact() { + final List modules = new ArrayList(); + + for (Module module : myContext.getModulesProvider().getModules()) { + final AndroidFacet facet = AndroidFacet.getInstance(module); + + if (facet != null && !facet.getConfiguration().LIBRARY_PROJECT) { + modules.add(module); + } + } + + final AndroidFacet facet = AndroidArtifactUtil.chooseAndroidApplicationModule(myContext.getProject(), modules); + if (facet == null) { + return null; + } + + final CompositePackagingElement rootElement = + AndroidApplicationArtifactType.this.createRootElement(facet.getModule().getName()); + rootElement.addFirstChild(new AndroidFinalPackageElement(myContext.getProject(), facet)); + return new NewArtifactConfiguration(rootElement, facet.getModule().getName(), AndroidApplicationArtifactType.this); + } + } +} diff --git a/plugins/android/src/org/jetbrains/android/compiler/artifact/AndroidArtifactPropertiesEditor.form b/plugins/android/src/org/jetbrains/android/compiler/artifact/AndroidArtifactPropertiesEditor.form new file mode 100644 index 000000000000..cef9f344b696 --- /dev/null +++ b/plugins/android/src/org/jetbrains/android/compiler/artifact/AndroidArtifactPropertiesEditor.form @@ -0,0 +1,157 @@ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
diff --git a/plugins/android/src/org/jetbrains/android/compiler/artifact/AndroidArtifactPropertiesEditor.java b/plugins/android/src/org/jetbrains/android/compiler/artifact/AndroidArtifactPropertiesEditor.java new file mode 100644 index 000000000000..8c86c94895ef --- /dev/null +++ b/plugins/android/src/org/jetbrains/android/compiler/artifact/AndroidArtifactPropertiesEditor.java @@ -0,0 +1,260 @@ +package org.jetbrains.android.compiler.artifact; + +import com.android.annotations.NonNull; +import com.intellij.CommonBundle; +import com.intellij.openapi.diagnostic.Logger; +import com.intellij.openapi.fileChooser.FileChooser; +import com.intellij.openapi.fileChooser.FileChooserDescriptor; +import com.intellij.openapi.fileChooser.FileChooserDescriptorFactory; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.ui.DialogWrapper; +import com.intellij.openapi.ui.Messages; +import com.intellij.openapi.ui.TextFieldWithBrowseButton; +import com.intellij.openapi.util.io.FileUtil; +import com.intellij.openapi.vfs.LocalFileSystem; +import com.intellij.openapi.vfs.VfsUtilCore; +import com.intellij.openapi.vfs.VirtualFile; +import com.intellij.packaging.ui.ArtifactPropertiesEditor; +import com.intellij.util.ui.UIUtil; +import org.jetbrains.android.util.AndroidUtils; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import javax.swing.*; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.io.*; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.cert.CertificateException; +import java.util.List; + +/** + * @author Eugene.Kudelevsky + */ +public class AndroidArtifactPropertiesEditor extends ArtifactPropertiesEditor { + private static final Logger LOG = Logger.getInstance("#org.jetbrains.android.compiler.artifact.AndroidApplicationArtifactProperties"); + + private final AndroidApplicationArtifactProperties myProperties; + + private JPanel myPanel; + private JRadioButton myDebugRadio; + private JRadioButton myReleaseSignedRadio; + private JRadioButton myReleaseUnsignedRadio; + private JPanel myReleaseKeyPanel; + private JPasswordField myKeyStorePasswordField; + private JTextField myKeyStorePathField; + private JPasswordField myKeyPasswordField; + private TextFieldWithBrowseButton myKeyAliasField; + private JButton myLoadKeyStoreButton; + private JButton myCreateKeyStoreButton; + + public AndroidArtifactPropertiesEditor(@NonNull AndroidApplicationArtifactProperties properties, @NotNull final Project project) { + myProperties = properties; + + final ActionListener listener = new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + UIUtil.setEnabled(myReleaseKeyPanel, myReleaseSignedRadio.isSelected(), true); + } + }; + myDebugRadio.addActionListener(listener); + myReleaseUnsignedRadio.addActionListener(listener); + myReleaseSignedRadio.addActionListener(listener); + + myLoadKeyStoreButton.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + final String defaultPath = getKeyStorePath(); + final VirtualFile defaultFile = LocalFileSystem.getInstance().findFileByPath(defaultPath); + final FileChooserDescriptor descriptor = FileChooserDescriptorFactory.createSingleFileNoJarsDescriptor(); + final VirtualFile file = FileChooser.chooseFile(descriptor, myPanel, project, defaultFile); + if (file != null) { + myKeyStorePathField.setText(FileUtil.toSystemDependentName(file.getPath())); + } + } + }); + + myCreateKeyStoreButton.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + final NewKeyStoreDialog dialog = new NewKeyStoreDialog(project, myKeyStorePathField.getText()); + dialog.show(); + + if (dialog.getExitCode() == DialogWrapper.OK_EXIT_CODE) { + myKeyStorePathField.setText(dialog.getKeyStorePath()); + myKeyStorePasswordField.setText(String.valueOf(dialog.getKeyStorePassword())); + myKeyAliasField.setText(dialog.getKeyAlias()); + myKeyPasswordField.setText(String.valueOf(dialog.getKeyPassword())); + } + } + }); + + myKeyAliasField.getButton().addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + final List keys = loadExistingKeys(); + if (keys == null) { + return; + } + final ChooseKeyDialog dialog = + new ChooseKeyDialog(project, getKeyStorePath(), myKeyStorePasswordField.getPassword(), keys, getKeyAlias()); + dialog.show(); + + if (dialog.getExitCode() == DialogWrapper.OK_EXIT_CODE) { + final String chosenKey = dialog.getChosenKey(); + if (chosenKey != null) { + myKeyAliasField.setText(chosenKey); + } + + final char[] password = dialog.getChosenKeyPassword(); + if (password != null) { + myKeyPasswordField.setText(String.valueOf(password)); + } + } + } + }); + } + + private String getKeyStorePath() { + return myKeyStorePathField.getText().trim(); + } + + @Nullable + private List loadExistingKeys() { + final String errorPrefix = "Cannot load key store: "; + InputStream is = null; + try { + //noinspection IOResourceOpenedButNotSafelyClosed + is = new FileInputStream(new File(getKeyStorePath())); + final KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType()); + keyStore.load(is, myKeyStorePasswordField.getPassword()); + return AndroidUtils.toList(keyStore.aliases()); + } + catch (KeyStoreException e) { + Messages.showErrorDialog(myPanel, errorPrefix + e.getMessage(), CommonBundle.getErrorTitle()); + return null; + } + catch (FileNotFoundException e) { + Messages.showErrorDialog(myPanel, errorPrefix + e.getMessage(), CommonBundle.getErrorTitle()); + return null; + } + catch (CertificateException e) { + Messages.showErrorDialog(myPanel, errorPrefix + e.getMessage(), CommonBundle.getErrorTitle()); + return null; + } + catch (NoSuchAlgorithmException e) { + Messages.showErrorDialog(myPanel, errorPrefix + e.getMessage(), CommonBundle.getErrorTitle()); + return null; + } + catch (IOException e) { + Messages.showErrorDialog(myPanel, errorPrefix + e.getMessage(), CommonBundle.getErrorTitle()); + return null; + } + finally { + if (is != null) { + try { + is.close(); + } + catch (IOException e) { + LOG.info(e); + } + } + } + } + + @Override + public String getTabName() { + return "Android"; + } + + @Override + public JComponent createComponent() { + return myPanel; + } + + @Override + public boolean isModified() { + return getSigningMode() != myProperties.getSigningMode() || + !getKeyStoreFileUrl().equals(myProperties.getKeyStoreUrl()) || + !getKeyStorePassword().equals(myProperties.getPlainKeystorePassword()) || + !getKeyAlias().equals(myProperties.getKeyAlias()) || + !getKeyPassword().equals(myProperties.getPlainKeyPassword()); + } + + @Override + public void apply() { + myProperties.setSigningMode(getSigningMode()); + myProperties.setKeyStoreUrl(getKeyStoreFileUrl()); + myProperties.setPlainKeystorePassword(getKeyStorePassword()); + myProperties.setKeyAlias(getKeyAlias()); + myProperties.setPlainKeyPassword(getKeyPassword()); + } + + @Override + public void reset() { + switch (myProperties.getSigningMode()) { + case RELEASE_UNSIGNED: + myReleaseUnsignedRadio.setSelected(true); + myDebugRadio.setSelected(false); + myReleaseSignedRadio.setSelected(false); + break; + case DEBUG: + myReleaseUnsignedRadio.setSelected(false); + myDebugRadio.setSelected(true); + myReleaseSignedRadio.setSelected(false); + break; + case RELEASE_SIGNED: + myReleaseUnsignedRadio.setSelected(false); + myDebugRadio.setSelected(false); + myReleaseSignedRadio.setSelected(true); + break; + } + final String keyStoreUrl = myProperties.getKeyStoreUrl(); + myKeyStorePathField.setText(keyStoreUrl != null ? VfsUtilCore.urlToPath(keyStoreUrl) : ""); + myKeyStorePasswordField.setText(myProperties.getPlainKeystorePassword()); + + final String keyAlias = myProperties.getKeyAlias(); + myKeyAliasField.setText(keyAlias != null ? keyAlias : ""); + myKeyPasswordField.setText(myProperties.getPlainKeyPassword()); + + UIUtil.setEnabled(myReleaseKeyPanel, myProperties.getSigningMode() == AndroidArtifactSigningMode.RELEASE_SIGNED, true); + } + + @Override + public void disposeUIResources() { + } + + @NotNull + private AndroidArtifactSigningMode getSigningMode() { + if (myDebugRadio.isSelected()) { + return AndroidArtifactSigningMode.DEBUG; + } + else if (myReleaseSignedRadio.isSelected()) { + return AndroidArtifactSigningMode.RELEASE_SIGNED; + } + return AndroidArtifactSigningMode.RELEASE_UNSIGNED; + } + + @NotNull + private String getKeyStoreFileUrl() { + final String path = getKeyStorePath(); + return VfsUtilCore.pathToUrl(path); + } + + @NotNull + private String getKeyStorePassword() { + return String.valueOf(myKeyStorePasswordField.getPassword()); + } + + @NotNull + private String getKeyAlias() { + return myKeyAliasField.getText().trim(); + } + + @NotNull + private String getKeyPassword() { + return String.valueOf(myKeyPasswordField.getPassword()); + } +} diff --git a/plugins/android/src/org/jetbrains/android/compiler/artifact/AndroidArtifactPropertiesProvider.java b/plugins/android/src/org/jetbrains/android/compiler/artifact/AndroidArtifactPropertiesProvider.java new file mode 100644 index 000000000000..84161bd24df5 --- /dev/null +++ b/plugins/android/src/org/jetbrains/android/compiler/artifact/AndroidArtifactPropertiesProvider.java @@ -0,0 +1,30 @@ +package org.jetbrains.android.compiler.artifact; + +import com.intellij.packaging.artifacts.ArtifactProperties; +import com.intellij.packaging.artifacts.ArtifactPropertiesProvider; +import com.intellij.packaging.artifacts.ArtifactType; +import org.jetbrains.annotations.NotNull; + +/** + * @author Eugene.Kudelevsky + */ +public class AndroidArtifactPropertiesProvider extends ArtifactPropertiesProvider { + protected AndroidArtifactPropertiesProvider() { + super("android-properties"); + } + + @Override + public boolean isAvailableFor(@NotNull ArtifactType type) { + return type instanceof AndroidApplicationArtifactType; + } + + @NotNull + @Override + public ArtifactProperties createProperties(@NotNull ArtifactType artifactType) { + return new AndroidApplicationArtifactProperties(); + } + + public static AndroidArtifactPropertiesProvider getInstance() { + return EP_NAME.findExtension(AndroidArtifactPropertiesProvider.class); + } +} diff --git a/plugins/android/src/org/jetbrains/android/compiler/artifact/AndroidArtifactSigningCompiler.java b/plugins/android/src/org/jetbrains/android/compiler/artifact/AndroidArtifactSigningCompiler.java new file mode 100644 index 000000000000..bacf8b670933 --- /dev/null +++ b/plugins/android/src/org/jetbrains/android/compiler/artifact/AndroidArtifactSigningCompiler.java @@ -0,0 +1,122 @@ +package org.jetbrains.android.compiler.artifact; + +import com.intellij.openapi.application.ApplicationManager; +import com.intellij.openapi.compiler.CompileContext; +import com.intellij.openapi.compiler.CompileScope; +import com.intellij.openapi.compiler.PackagingCompiler; +import com.intellij.openapi.compiler.ValidityState; +import com.intellij.openapi.util.Computable; +import com.intellij.openapi.vfs.VirtualFile; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.io.DataInput; +import java.io.DataOutput; +import java.io.IOException; + +/** + * @author Eugene.Kudelevsky + */ +public class AndroidArtifactSigningCompiler implements PackagingCompiler { + @Override + public void processOutdatedItem(CompileContext context, String url, @Nullable ValidityState state) { + } + + @NotNull + @Override + public ProcessingItem[] getProcessingItems(CompileContext context) { + return ApplicationManager.getApplication().runReadAction(new Computable() { + @Override + public ProcessingItem[] compute() { + return null; + } + }); + } + + @Override + public ProcessingItem[] process(CompileContext context, ProcessingItem[] items) { + return new ProcessingItem[0]; //To change body of implemented methods use File | Settings | File Templates. + } + + @NotNull + @Override + public String getDescription() { + return "Android Artifact Signing Compiler"; + } + + @Override + public boolean validateConfiguration(CompileScope scope) { + return true; + } + + @Override + public ValidityState createValidityState(DataInput in) throws IOException { + return new MyValidityState(in); + } + + private static class MyProcessingItem implements ProcessingItem { + private final VirtualFile myApkFile; + private final AndroidArtifactSigningMode mySigningMode; + private final String myDebugKeyStorePath; + private final MyValidityState myValidityState; + + private MyProcessingItem(@NotNull VirtualFile apkFile, + @NotNull AndroidArtifactSigningMode signingMode, + @Nullable String debugKeyStorePath) { + myApkFile = apkFile; + mySigningMode = signingMode; + myDebugKeyStorePath = debugKeyStorePath; + + myValidityState = new MyValidityState(myApkFile.getModificationStamp(), + mySigningMode.name(), + myDebugKeyStorePath); + } + + @NotNull + @Override + public VirtualFile getFile() { + return myApkFile; + } + + @Override + public ValidityState getValidityState() { + return myValidityState; + } + } + + private static class MyValidityState implements ValidityState { + private final long myApkFileTimestamp; + private final String mySigningMode; + private final String myDebugKeyStorePath; + + private MyValidityState(long apkFileTimestamp, @NotNull String signingMode, @NotNull String debugKeyStorePath) { + myApkFileTimestamp = apkFileTimestamp; + mySigningMode = signingMode; + myDebugKeyStorePath = debugKeyStorePath; + } + + private MyValidityState(@NotNull DataInput in) throws IOException { + myApkFileTimestamp = in.readLong(); + mySigningMode = in.readUTF(); + myDebugKeyStorePath = in.readUTF(); + } + + @Override + public boolean equalsTo(ValidityState otherState) { + if (!(otherState instanceof MyValidityState)) { + return false; + } + final MyValidityState state = (MyValidityState)otherState; + return state.myApkFileTimestamp == myApkFileTimestamp && + state.mySigningMode.equals(mySigningMode) && + state.myDebugKeyStorePath.equals(myDebugKeyStorePath); + } + + @Override + public void save(DataOutput out) throws IOException { + out.writeLong(myApkFileTimestamp); + out.writeUTF(mySigningMode); + out.writeUTF(myDebugKeyStorePath); + } + } +} diff --git a/plugins/android/src/org/jetbrains/android/compiler/artifact/AndroidArtifactSigningMode.java b/plugins/android/src/org/jetbrains/android/compiler/artifact/AndroidArtifactSigningMode.java new file mode 100644 index 000000000000..4499d7a60d4d --- /dev/null +++ b/plugins/android/src/org/jetbrains/android/compiler/artifact/AndroidArtifactSigningMode.java @@ -0,0 +1,8 @@ +package org.jetbrains.android.compiler.artifact; + +/** + * @author Eugene.Kudelevsky + */ +public enum AndroidArtifactSigningMode { + DEBUG, RELEASE_UNSIGNED, RELEASE_SIGNED +} diff --git a/plugins/android/src/org/jetbrains/android/compiler/artifact/AndroidArtifactUtil.java b/plugins/android/src/org/jetbrains/android/compiler/artifact/AndroidArtifactUtil.java new file mode 100644 index 000000000000..85bf6c281842 --- /dev/null +++ b/plugins/android/src/org/jetbrains/android/compiler/artifact/AndroidArtifactUtil.java @@ -0,0 +1,56 @@ +package org.jetbrains.android.compiler.artifact; + +import com.intellij.CommonBundle; +import com.intellij.openapi.module.Module; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.roots.ui.configuration.ChooseModulesDialog; +import com.intellij.openapi.ui.Messages; +import com.intellij.packaging.artifacts.Artifact; +import com.intellij.packaging.impl.artifacts.ArtifactUtil; +import com.intellij.packaging.ui.ArtifactEditorContext; +import com.intellij.util.Processor; +import org.jetbrains.android.facet.AndroidFacet; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.List; + +/** + * @author Eugene.Kudelevsky + */ +public class AndroidArtifactUtil { + private AndroidArtifactUtil() { + } + + public static boolean containsAndroidPackage(ArtifactEditorContext editorContext, Artifact artifact) { + return !ArtifactUtil + .processPackagingElements(artifact, AndroidFinalPackageElementType.getInstance(), new Processor() { + public boolean process(AndroidFinalPackageElement e) { + return false; + } + }, editorContext, true); + } + + @Nullable + public static AndroidFacet chooseAndroidApplicationModule(@NotNull Project project, @NotNull List modules) { + final ChooseModulesDialog dialog = new ChooseModulesDialog(project, modules, "Select Module", + "Selected Android application module will be included in the created artifact with all dependencies"); + dialog.setSingleSelectionMode(); + dialog.show(); + final List selected = dialog.getChosenElements(); + if (selected.isEmpty()) { + return null; + } + assert selected.size() == 1; + final Module module = selected.get(0); + final String moduleName = module.getName(); + + final AndroidFacet facet = AndroidFacet.getInstance(module); + if (facet == null) { + final String message = "Cannot find Android facet for module " + moduleName; + Messages.showErrorDialog(project, message, CommonBundle.getErrorTitle()); + return null; + } + return facet; + } +} diff --git a/plugins/android/src/org/jetbrains/android/compiler/artifact/AndroidFinalPackageElement.java b/plugins/android/src/org/jetbrains/android/compiler/artifact/AndroidFinalPackageElement.java new file mode 100644 index 000000000000..3c719ed56d90 --- /dev/null +++ b/plugins/android/src/org/jetbrains/android/compiler/artifact/AndroidFinalPackageElement.java @@ -0,0 +1,154 @@ +package org.jetbrains.android.compiler.artifact; + +import com.intellij.compiler.ant.Generator; +import com.intellij.facet.pointers.FacetPointer; +import com.intellij.facet.pointers.FacetPointersManager; +import com.intellij.openapi.module.Module; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.util.io.FileUtil; +import com.intellij.openapi.vfs.JarFileSystem; +import com.intellij.openapi.vfs.VirtualFile; +import com.intellij.packaging.artifacts.ArtifactType; +import com.intellij.packaging.elements.*; +import com.intellij.packaging.impl.elements.FacetBasedPackagingElement; +import com.intellij.packaging.impl.elements.ModuleOutputPackagingElement; +import com.intellij.packaging.impl.ui.DelegatedPackagingElementPresentation; +import com.intellij.packaging.ui.ArtifactEditorContext; +import com.intellij.packaging.ui.PackagingElementPresentation; +import com.intellij.util.xmlb.annotations.Attribute; +import org.jetbrains.android.facet.AndroidFacet; +import org.jetbrains.android.facet.AndroidRootUtil; +import org.jetbrains.android.util.AndroidCommonUtils; +import org.jetbrains.annotations.NonNls; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.Collection; +import java.util.Collections; +import java.util.List; + +/** + * @author Eugene.Kudelevsky + */ +public class AndroidFinalPackageElement extends PackagingElement + implements FacetBasedPackagingElement, ModuleOutputPackagingElement { + + @NonNls static final String FACET_ATTRIBUTE = "facet"; + + private FacetPointer myFacetPointer; + private final Project myProject; + + public AndroidFinalPackageElement(@NotNull Project project, @Nullable AndroidFacet facet) { + super(AndroidFinalPackageElementType.getInstance()); + myProject = project; + myFacetPointer = facet != null ? FacetPointersManager.getInstance(myProject).create(facet) : null; + } + + @Override + public PackagingElementPresentation createPresentation(@NotNull ArtifactEditorContext context) { + return new DelegatedPackagingElementPresentation(new AndroidFinalPackagePresentation(myFacetPointer)); + } + + @Nullable + private String getApkPath() { + if (myFacetPointer == null) { + return null; + } + + final AndroidFacet facet = myFacetPointer.getFacet(); + if (facet == null) { + return null; + } + + final String apkPath = AndroidRootUtil.getApkPath(facet); + final String path = apkPath != null + ? AndroidCommonUtils.addSuffixToFileName(apkPath, AndroidCommonUtils.ANDROID_FINAL_PACKAGE_FOR_ARTIFACT_SUFFIX) + : null; + return path != null + ? FileUtil.toSystemIndependentName(path) + "!/" + : null; + } + + @Override + public List computeAntInstructions(@NotNull PackagingElementResolvingContext resolvingContext, + @NotNull AntCopyInstructionCreator creator, + @NotNull ArtifactAntGenerationContext generationContext, + @NotNull ArtifactType artifactType) { + final String apkPath = getApkPath(); + if (apkPath != null) { + return Collections.singletonList(creator.createExtractedDirectoryInstruction(apkPath)); + } + return Collections.emptyList(); + } + + @Override + public void computeIncrementalCompilerInstructions(@NotNull IncrementalCompilerInstructionCreator creator, + @NotNull PackagingElementResolvingContext resolvingContext, + @NotNull ArtifactIncrementalCompilerContext compilerContext, + @NotNull ArtifactType artifactType) { + final String apkPath = getApkPath(); + if (apkPath != null) { + final VirtualFile apk = JarFileSystem.getInstance().findFileByPath(apkPath); + if (apk != null && apk.isValid() && apk.isDirectory()) { + creator.addDirectoryCopyInstructions(apk); + } + } + } + + @Nullable + public AndroidFacet getFacet() { + return myFacetPointer != null ? myFacetPointer.getFacet() : null; + } + + @Override + public boolean isEqualTo(@NotNull PackagingElement element) { + if (!(element instanceof AndroidFinalPackageElement)) { + return false; + } + final AndroidFinalPackageElement packageElement = (AndroidFinalPackageElement)element; + + return myFacetPointer == null + ? packageElement.myFacetPointer == null + : myFacetPointer.equals(packageElement.myFacetPointer); + } + + public AndroidFinalPackageElementState getState() { + final AndroidFinalPackageElementState state = new AndroidFinalPackageElementState(); + state.myFacetPointer = myFacetPointer != null ? myFacetPointer.getId() : null; + return state; + } + + @Override + public AndroidFacet findFacet(@NotNull PackagingElementResolvingContext context) { + return myFacetPointer != null ? myFacetPointer.findFacet(context.getModulesProvider(), context.getFacetsProvider()) : null; + } + + public void loadState(AndroidFinalPackageElementState state) { + myFacetPointer = state.myFacetPointer != null + ? FacetPointersManager.getInstance(myProject).create(state.myFacetPointer) + : null; + } + + @Override + public String getModuleName() { + return myFacetPointer != null ? myFacetPointer.getModuleName() : null; + } + + @Override + public Module findModule(PackagingElementResolvingContext context) { + final AndroidFacet facet = findFacet(context); + return facet != null ? facet.getModule() : null; + } + + @NotNull + @Override + public Collection getSourceRoots(PackagingElementResolvingContext context) { + return Collections.emptyList(); + } + + public static class AndroidFinalPackageElementState { + + @Attribute(FACET_ATTRIBUTE) + public String myFacetPointer; + } +} diff --git a/plugins/android/src/org/jetbrains/android/compiler/artifact/AndroidFinalPackageElementType.java b/plugins/android/src/org/jetbrains/android/compiler/artifact/AndroidFinalPackageElementType.java new file mode 100644 index 000000000000..e1e76d027d5f --- /dev/null +++ b/plugins/android/src/org/jetbrains/android/compiler/artifact/AndroidFinalPackageElementType.java @@ -0,0 +1,88 @@ +package org.jetbrains.android.compiler.artifact; + +import com.intellij.openapi.module.Module; +import com.intellij.openapi.project.Project; +import com.intellij.packaging.artifacts.Artifact; +import com.intellij.packaging.elements.CompositePackagingElement; +import com.intellij.packaging.elements.PackagingElement; +import com.intellij.packaging.elements.PackagingElementType; +import com.intellij.packaging.ui.ArtifactEditorContext; +import org.jetbrains.android.facet.AndroidFacet; +import org.jetbrains.android.util.AndroidUtils; +import org.jetbrains.annotations.NonNls; +import org.jetbrains.annotations.NotNull; + +import javax.swing.*; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; + +/** + * @author Eugene.Kudelevsky + */ +public class AndroidFinalPackageElementType extends PackagingElementType { + @NonNls public static final String TYPE_ID = "android-final-package"; + + protected AndroidFinalPackageElementType() { + super(TYPE_ID, "Android Final Package"); + } + + public static AndroidFinalPackageElementType getInstance() { + return getInstance(AndroidFinalPackageElementType.class); + } + + @Override + public Icon getCreateElementIcon() { + return AndroidUtils.ANDROID_ICON; + } + + @Override + public boolean canCreate(@NotNull ArtifactEditorContext context, @NotNull Artifact artifact) { + return getAndroidApplicationFacets(context, context.getModulesProvider().getModules()).size() > 0 && + !AndroidArtifactUtil.containsAndroidPackage(context, artifact); + } + + @NotNull + private static List getAndroidApplicationFacets(@NotNull ArtifactEditorContext context, @NotNull Module[] modules) { + final List result = new ArrayList(); + for (Module module : modules) { + for (AndroidFacet facet : context.getFacetsProvider().getFacetsByType(module, AndroidFacet.ID)) { + if (!facet.getConfiguration().LIBRARY_PROJECT) { + result.add(facet); + } + } + } + return result; + } + + @NotNull + private static List facetsToModules(@NotNull Collection facets) { + final List result = new ArrayList(facets.size()); + for (AndroidFacet facet : facets) { + result.add(facet.getModule()); + } + return result; + } + + @NotNull + @Override + public List> chooseAndCreate(@NotNull ArtifactEditorContext context, + @NotNull Artifact artifact, + @NotNull CompositePackagingElement parent) { + final List facets = getAndroidApplicationFacets(context, context.getModulesProvider().getModules()); + final List modules = facetsToModules(facets); + + final AndroidFacet facet = AndroidArtifactUtil.chooseAndroidApplicationModule(context.getProject(), modules); + if (facet == null) { + return Collections.emptyList(); + } + return Collections.singletonList(new AndroidFinalPackageElement(context.getProject(), facet)); + } + + @NotNull + @Override + public AndroidFinalPackageElement createEmpty(@NotNull Project project) { + return new AndroidFinalPackageElement(project, null); + } +} diff --git a/plugins/android/src/org/jetbrains/android/compiler/artifact/AndroidFinalPackagePresentation.java b/plugins/android/src/org/jetbrains/android/compiler/artifact/AndroidFinalPackagePresentation.java new file mode 100644 index 000000000000..d43b9cb2c08b --- /dev/null +++ b/plugins/android/src/org/jetbrains/android/compiler/artifact/AndroidFinalPackagePresentation.java @@ -0,0 +1,40 @@ +package org.jetbrains.android.compiler.artifact; + +import com.intellij.facet.pointers.FacetPointer; +import com.intellij.ide.projectView.PresentationData; +import com.intellij.packaging.ui.SourceItemPresentation; +import com.intellij.packaging.ui.SourceItemWeights; +import com.intellij.ui.SimpleTextAttributes; +import org.jetbrains.android.facet.AndroidFacet; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +/** + * @author Eugene.Kudelevsky + */ +class AndroidFinalPackagePresentation extends SourceItemPresentation { + private final FacetPointer myFacetPointer; + + public AndroidFinalPackagePresentation(@Nullable FacetPointer facetPointer) { + myFacetPointer = facetPointer; + } + + @Override + public String getPresentableName() { + final String moduleName = myFacetPointer != null ? myFacetPointer.getModuleName() : ""; + return "'" + moduleName + "' Android final package"; + } + + @Override + public void render(@NotNull PresentationData presentationData, + SimpleTextAttributes mainAttributes, + SimpleTextAttributes commentAttributes) { + presentationData.setIcons(AndroidFacet.getFacetType().getIcon()); + presentationData.addText(getPresentableName(), mainAttributes); + } + + @Override + public int getWeight() { + return SourceItemWeights.LIBRARY_WEIGHT - 5; + } +} diff --git a/plugins/android/src/org/jetbrains/android/compiler/artifact/AndroidSourceItemsProvider.java b/plugins/android/src/org/jetbrains/android/compiler/artifact/AndroidSourceItemsProvider.java new file mode 100644 index 000000000000..4c2528464ed6 --- /dev/null +++ b/plugins/android/src/org/jetbrains/android/compiler/artifact/AndroidSourceItemsProvider.java @@ -0,0 +1,56 @@ +package org.jetbrains.android.compiler.artifact; + +import com.intellij.facet.pointers.FacetPointersManager; +import com.intellij.openapi.module.Module; +import com.intellij.openapi.roots.ui.configuration.artifacts.sourceItems.FacetBasedPackagingSourceItemsProvider; +import com.intellij.openapi.roots.ui.configuration.artifacts.sourceItems.ModuleSourceItemGroup; +import com.intellij.packaging.artifacts.Artifact; +import com.intellij.packaging.ui.ArtifactEditorContext; +import com.intellij.packaging.ui.PackagingSourceItem; +import org.jetbrains.android.facet.AndroidFacet; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; + +/** + * @author Eugene.Kudelevsky + */ +public class AndroidSourceItemsProvider extends FacetBasedPackagingSourceItemsProvider { + public AndroidSourceItemsProvider() { + super(AndroidFacet.ID, AndroidFinalPackageElementType.getInstance()); + } + + @Override + protected AndroidFinalPackagePresentation createPresentation(AndroidFacet facet) { + return new AndroidFinalPackagePresentation(FacetPointersManager.getInstance(facet.getModule().getProject()).create(facet)); + } + + @Override + protected AndroidFinalPackageElement createElement(ArtifactEditorContext context, AndroidFacet facet) { + return new AndroidFinalPackageElement(context.getProject(), facet); + } + + @NotNull + @Override + public Collection getSourceItems(@NotNull ArtifactEditorContext editorContext, + @NotNull Artifact artifact, + @Nullable PackagingSourceItem parent) { + + if (parent instanceof ModuleSourceItemGroup && !AndroidArtifactUtil.containsAndroidPackage(editorContext, artifact)) { + final Module module = ((ModuleSourceItemGroup)parent).getModule(); + final Set facets = + new HashSet(editorContext.getFacetsProvider().getFacetsByType(module, AndroidFacet.ID)); + return Collections.singletonList(new FacetBasedSourceItem(this, facets.iterator().next())); + } + return Collections.emptyList(); + } + + @Override + protected AndroidFacet getFacet(AndroidFinalPackageElement element) { + return element.getFacet(); + } +} diff --git a/plugins/android/src/org/jetbrains/android/compiler/artifact/ChooseKeyDialog.form b/plugins/android/src/org/jetbrains/android/compiler/artifact/ChooseKeyDialog.form new file mode 100644 index 000000000000..0b5728b4ae74 --- /dev/null +++ b/plugins/android/src/org/jetbrains/android/compiler/artifact/ChooseKeyDialog.form @@ -0,0 +1,57 @@ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
diff --git a/plugins/android/src/org/jetbrains/android/compiler/artifact/ChooseKeyDialog.java b/plugins/android/src/org/jetbrains/android/compiler/artifact/ChooseKeyDialog.java new file mode 100644 index 000000000000..7b0695955515 --- /dev/null +++ b/plugins/android/src/org/jetbrains/android/compiler/artifact/ChooseKeyDialog.java @@ -0,0 +1,129 @@ +package org.jetbrains.android.compiler.artifact; + +import com.intellij.CommonBundle; +import com.intellij.ide.wizard.CommitStepException; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.ui.DialogWrapper; +import com.intellij.openapi.ui.Messages; +import com.intellij.ui.CollectionComboBoxModel; +import com.intellij.ui.components.JBRadioButton; +import com.intellij.util.ui.UIUtil; +import org.jetbrains.android.exportSignedPackage.NewKeyForm; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import javax.swing.*; +import java.awt.*; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.util.List; + +/** + * @author Eugene.Kudelevsky + */ +public class ChooseKeyDialog extends DialogWrapper { + private JPanel myNewKeyPanel; + private JBRadioButton myCreateNewKeyRadioButton; + private JBRadioButton myUseExistingKeyRadioButton; + private JComboBox myKeyCombo; + private JPanel myPanel; + + private final NewKeyForm myNewKeyForm = new MyNewKeyForm(); + private final Project myProject; + private final String myKeyStorePath; + private final char[] myKeyStorePassword; + private final List myExistingKeys; + + public ChooseKeyDialog(@NotNull Project project, + @NotNull String keyStorePath, + @NotNull char[] password, + @NotNull List existingKeys, + @Nullable String keyToSelect) { + super(project); + myProject = project; + myKeyStorePath = keyStorePath; + myKeyStorePassword = password; + myExistingKeys = existingKeys; + myKeyCombo.setModel(new CollectionComboBoxModel(existingKeys, existingKeys.get(0))); + + if (keyToSelect != null && existingKeys.contains(keyToSelect)) { + myKeyCombo.setSelectedItem(keyToSelect); + } + myNewKeyPanel.add(myNewKeyForm.getContentPanel(), BorderLayout.CENTER); + + final ActionListener listener = new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + UIUtil.setEnabled(myNewKeyPanel, myCreateNewKeyRadioButton.isSelected(), true); + } + }; + myCreateNewKeyRadioButton.addActionListener(listener); + myUseExistingKeyRadioButton.addActionListener(listener); + + final boolean useExisting = existingKeys.size() > 0; + myUseExistingKeyRadioButton.setSelected(useExisting); + myCreateNewKeyRadioButton.setSelected(!useExisting); + UIUtil.setEnabled(myNewKeyPanel, !useExisting, true); + + setTitle("Create New Key"); + init(); + } + + @Override + protected JComponent createCenterPanel() { + return myPanel; + } + + @Override + protected void doOKAction() { + if (myCreateNewKeyRadioButton.isSelected()) { + try { + myNewKeyForm.createKey(); + } + catch (CommitStepException e) { + Messages.showErrorDialog(myPanel, e.getMessage(), CommonBundle.getErrorTitle()); + return; + } + } + super.doOKAction(); + } + + @Nullable + public String getChosenKey() { + return myUseExistingKeyRadioButton.isSelected() + ? (String)myKeyCombo.getSelectedItem() + : myNewKeyForm.getKeyAlias(); + } + + @Nullable + public char[] getChosenKeyPassword() { + return myCreateNewKeyRadioButton.isSelected() + ? myNewKeyForm.getKeyPassword() + : null; + } + + private class MyNewKeyForm extends NewKeyForm { + @Override + protected List getExistingKeyAliasList() { + return myExistingKeys; + } + + @NotNull + @Override + protected Project getProject() { + return myProject; + } + + @NotNull + @Override + protected char[] getKeyStorePassword() { + return myKeyStorePassword; + } + + @NotNull + @Override + protected String getKeyStoreLocation() { + return myKeyStorePath; + } + } +} diff --git a/plugins/android/src/org/jetbrains/android/compiler/artifact/NewKeyStoreDialog.form b/plugins/android/src/org/jetbrains/android/compiler/artifact/NewKeyStoreDialog.form new file mode 100644 index 000000000000..ade0f50564b1 --- /dev/null +++ b/plugins/android/src/org/jetbrains/android/compiler/artifact/NewKeyStoreDialog.form @@ -0,0 +1,78 @@ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
diff --git a/plugins/android/src/org/jetbrains/android/compiler/artifact/NewKeyStoreDialog.java b/plugins/android/src/org/jetbrains/android/compiler/artifact/NewKeyStoreDialog.java new file mode 100644 index 000000000000..7ea7ae3a88be --- /dev/null +++ b/plugins/android/src/org/jetbrains/android/compiler/artifact/NewKeyStoreDialog.java @@ -0,0 +1,124 @@ +package org.jetbrains.android.compiler.artifact; + +import com.intellij.CommonBundle; +import com.intellij.ide.wizard.CommitStepException; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.ui.DialogWrapper; +import com.intellij.openapi.ui.Messages; +import com.intellij.openapi.ui.TextFieldWithBrowseButton; +import org.jetbrains.android.exportSignedPackage.NewKeyForm; +import org.jetbrains.android.util.AndroidBundle; +import org.jetbrains.android.util.AndroidUtils; +import org.jetbrains.android.util.SaveFileListener; +import org.jetbrains.annotations.NotNull; + +import javax.swing.*; +import java.awt.*; +import java.util.Collections; +import java.util.List; + +/** + * @author Eugene.Kudelevsky + */ +public class NewKeyStoreDialog extends DialogWrapper { + private JPanel myNewKeyPanel; + private JPanel myPanel; + private TextFieldWithBrowseButton myKeyStorePathField; + private JPasswordField myPasswordField; + private JPasswordField myConfirmedPassword; + + private final NewKeyForm myNewKeyForm; + private final Project myProject; + + public NewKeyStoreDialog(@NotNull Project project, @NotNull String defaultKeyStorePath) { + super(project); + myProject = project; + myKeyStorePathField.setText(defaultKeyStorePath); + setTitle("Create New Key Store"); + myNewKeyForm = new MyNewKeyForm(); + myNewKeyPanel.add(myNewKeyForm.getContentPanel(), BorderLayout.CENTER); + + myKeyStorePathField.addActionListener(new SaveFileListener(myPanel, myKeyStorePathField, AndroidBundle.message( + "android.extract.package.choose.keystore.title")) { + @Override + protected String getDefaultLocation() { + return getKeyStorePath(); + } + }); + init(); + } + + @Override + protected JComponent createCenterPanel() { + return myPanel; + } + + @Override + public JComponent getPreferredFocusedComponent() { + return myKeyStorePathField; + } + + @Override + protected void doOKAction() { + if (getKeyStorePath().length() == 0) { + Messages.showErrorDialog(myPanel, "Specify key store path", CommonBundle.getErrorTitle()); + return; + } + + try { + AndroidUtils.checkNewPassword(myPasswordField, myConfirmedPassword); + myNewKeyForm.createKey(); + } + catch (CommitStepException e) { + Messages.showErrorDialog(myPanel, e.getMessage(), CommonBundle.getErrorTitle()); + return; + } + super.doOKAction(); + } + + @NotNull + public String getKeyStorePath() { + return myKeyStorePathField.getText().trim(); + } + + @NotNull + public char[] getKeyStorePassword() { + return myPasswordField.getPassword(); + } + + @NotNull + public String getKeyAlias() { + return myNewKeyForm.getKeyAlias(); + } + + @NotNull + public char[] getKeyPassword() { + return myNewKeyForm.getKeyPassword(); + } + + private class MyNewKeyForm extends NewKeyForm { + + @Override + protected List getExistingKeyAliasList() { + return Collections.emptyList(); + } + + @NotNull + @Override + protected Project getProject() { + return myProject; + } + + @NotNull + @Override + protected char[] getKeyStorePassword() { + return NewKeyStoreDialog.this.getKeyStorePassword(); + } + + @NotNull + @Override + protected String getKeyStoreLocation() { + return getKeyStorePath(); + } + } +} diff --git a/plugins/android/src/org/jetbrains/android/exportSignedPackage/ApkStep.java b/plugins/android/src/org/jetbrains/android/exportSignedPackage/ApkStep.java index 06547b1bd90b..6c25426a7b9e 100644 --- a/plugins/android/src/org/jetbrains/android/exportSignedPackage/ApkStep.java +++ b/plugins/android/src/org/jetbrains/android/exportSignedPackage/ApkStep.java @@ -16,7 +16,6 @@ package org.jetbrains.android.exportSignedPackage; -import com.android.jarutils.SignedJarBuilder; import com.android.sdklib.SdkConstants; import com.intellij.CommonBundle; import com.intellij.execution.ExecutionException; @@ -50,7 +49,6 @@ import com.intellij.openapi.vfs.LocalFileSystem; import com.intellij.openapi.vfs.VirtualFile; import com.intellij.ui.components.JBLabel; import org.jetbrains.android.compiler.AndroidCompileUtil; -import org.jetbrains.android.compiler.AndroidPackagingCompiler; import org.jetbrains.android.compiler.AndroidProguardCompiler; import org.jetbrains.android.facet.AndroidFacet; import org.jetbrains.android.facet.AndroidFacetConfiguration; @@ -58,7 +56,6 @@ import org.jetbrains.android.facet.AndroidRootUtil; import org.jetbrains.android.sdk.AndroidPlatform; import org.jetbrains.android.util.AndroidBundle; import org.jetbrains.android.util.AndroidCommonUtils; -import org.jetbrains.android.util.SafeSignedJarBuilder; import org.jetbrains.android.util.SaveFileListener; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -68,12 +65,8 @@ import java.awt.*; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.io.File; -import java.io.FileInputStream; -import java.io.FileOutputStream; import java.io.IOException; import java.security.GeneralSecurityException; -import java.security.PrivateKey; -import java.security.cert.X509Certificate; /** * @author Eugene.Kudelevsky @@ -110,7 +103,7 @@ class ApkStep extends ExportSignedPackageWizardStep { myWizard = wizard; myApkPathLabel.setLabelFor(myApkPathField); myProguardConfigFilePathLabel.setLabelFor(myProguardConfigFilePathField); - + myApkPathField.getButton().addActionListener( new SaveFileListener(myContentPanel, myApkPathField, AndroidBundle.message("android.extract.package.choose.dest.apk")) { @Override @@ -140,7 +133,7 @@ class ApkStep extends ExportSignedPackageWizardStep { } } }); - + myProguardCheckBox.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { @@ -193,7 +186,7 @@ class ApkStep extends ExportSignedPackageWizardStep { myIncludeSystemProguardFileCheckBox.setVisible(AndroidCommonUtils.isIncludingInProguardSupported(sdkToolsRevision)); final String proguardCfgPath = properties.getValue(PROGUARD_CFG_PATH_PROPERTY); - if (proguardCfgPath != null && + if (proguardCfgPath != null && LocalFileSystem.getInstance().refreshAndFindFileByPath(proguardCfgPath) != null) { myProguardConfigFilePathField.setText(FileUtil.toSystemDependentName(proguardCfgPath)); final String includeSystemProguardFile = properties.getValue(INCLUDE_SYSTEM_PROGUARD_FILE_PROPERTY); @@ -274,35 +267,11 @@ class ApkStep extends ExportSignedPackageWizardStep { @SuppressWarnings({"IOResourceOpenedButNotSafelyClosed"}) private void createApk(File destFile) throws IOException, GeneralSecurityException { - final String srcApkPath = AndroidRootUtil.getApkPath(myWizard.getFacet()) + AndroidPackagingCompiler.UNSIGNED_SUFFIX; + final String srcApkPath = AndroidCompileUtil.getUnsignedApkPath(myWizard.getFacet()); final File srcApk = new File(FileUtil.toSystemDependentName(srcApkPath)); if (myWizard.isSigned()) { - FileOutputStream fos = new FileOutputStream(destFile); - PrivateKey privateKey = myWizard.getPrivateKey(); - assert privateKey != null; - X509Certificate certificate = myWizard.getCertificate(); - assert certificate != null; - SignedJarBuilder builder = new SafeSignedJarBuilder(fos, privateKey, certificate, destFile.getPath()); - FileInputStream fis = new FileInputStream(srcApk); - try { - builder.writeZip(fis, null); - builder.close(); - } - finally { - try { - fis.close(); - } - catch (IOException ignored) { - } - finally { - try { - fos.close(); - } - catch (IOException ignored) { - } - } - } + AndroidCommonUtils.signApk(srcApk, destFile, myWizard.getPrivateKey(), myWizard.getCertificate()); } else { FileUtil.copy(srcApk, destFile); diff --git a/plugins/android/src/org/jetbrains/android/exportSignedPackage/ExportSignedPackageWizardStep.java b/plugins/android/src/org/jetbrains/android/exportSignedPackage/ExportSignedPackageWizardStep.java index 8e542c7a8feb..a676bb78f1cb 100644 --- a/plugins/android/src/org/jetbrains/android/exportSignedPackage/ExportSignedPackageWizardStep.java +++ b/plugins/android/src/org/jetbrains/android/exportSignedPackage/ExportSignedPackageWizardStep.java @@ -18,11 +18,9 @@ package org.jetbrains.android.exportSignedPackage; import com.intellij.ide.wizard.CommitStepException; import com.intellij.ide.wizard.StepAdapter; -import org.jetbrains.android.util.AndroidBundle; import org.jetbrains.annotations.Nullable; import javax.swing.*; -import java.util.Arrays; /** * @author Eugene.Kudelevsky @@ -44,42 +42,8 @@ public abstract class ExportSignedPackageWizardStep extends StepAdapter { public abstract String getHelpId(); - protected static void checkNewPassword(JPasswordField passwordField, JPasswordField confirmedPasswordField) throws CommitStepException { - char[] password = passwordField.getPassword(); - char[] confirmedPassword = confirmedPasswordField.getPassword(); - try { - checkPassword(password); - if (password.length < 6) { - throw new CommitStepException(AndroidBundle.message("android.export.package.incorrect.password.length")); - } - if (!Arrays.equals(password, confirmedPassword)) { - throw new CommitStepException(AndroidBundle.message("android.export.package.passwords.not.match.error")); - } - } - finally { - Arrays.fill(password, '\0'); - Arrays.fill(confirmedPassword, '\0'); - } - } - protected abstract void commitForNext() throws CommitStepException; - protected static void checkPassword(char[] password) throws CommitStepException { - if (password.length == 0) { - throw new CommitStepException(AndroidBundle.message("android.export.package.specify.password.error")); - } - } - - protected static void checkPassword(JPasswordField passwordField) throws CommitStepException { - char[] password = passwordField.getPassword(); - try { - checkPassword(password); - } - finally { - Arrays.fill(password, '\0'); - } - } - @Override public Icon getIcon() { return null; diff --git a/plugins/android/src/org/jetbrains/android/exportSignedPackage/InitialKeyStep.java b/plugins/android/src/org/jetbrains/android/exportSignedPackage/InitialKeyStep.java index fc9e6de8540b..9a20ae2b17e0 100644 --- a/plugins/android/src/org/jetbrains/android/exportSignedPackage/InitialKeyStep.java +++ b/plugins/android/src/org/jetbrains/android/exportSignedPackage/InitialKeyStep.java @@ -22,6 +22,7 @@ import com.intellij.openapi.ui.Messages; import com.intellij.ui.CollectionComboBoxModel; import com.intellij.util.ui.UIUtil; import org.jetbrains.android.util.AndroidBundle; +import org.jetbrains.android.util.AndroidUtils; import javax.swing.*; import java.awt.event.ActionEvent; @@ -132,7 +133,7 @@ class InitialKeyStep extends ExportSignedPackageWizardStep { if (myAliasCombo.getSelectedItem() == null) { throw new CommitStepException(AndroidBundle.message("android.extract.package.select.key.alias.error")); } - checkPassword(password); + AndroidUtils.checkPassword(password); String alias = (String)myAliasCombo.getSelectedItem(); loadKey(alias, password); } diff --git a/plugins/android/src/org/jetbrains/android/exportSignedPackage/KeystoreStep.java b/plugins/android/src/org/jetbrains/android/exportSignedPackage/KeystoreStep.java index 1cd022ce4fad..f5a8cc6b02bb 100644 --- a/plugins/android/src/org/jetbrains/android/exportSignedPackage/KeystoreStep.java +++ b/plugins/android/src/org/jetbrains/android/exportSignedPackage/KeystoreStep.java @@ -26,6 +26,7 @@ import com.intellij.openapi.util.io.FileUtil; import com.intellij.openapi.vfs.LocalFileSystem; import com.intellij.openapi.vfs.VirtualFile; import org.jetbrains.android.util.AndroidBundle; +import org.jetbrains.android.util.AndroidUtils; import org.jetbrains.android.util.SaveFileListener; import org.jetbrains.annotations.Nullable; @@ -102,7 +103,7 @@ class KeystoreStep extends ExportSignedPackageWizardStep { private KeyStore checkExistingKeystoreOptionsAndCreateKeystore(File keystoreFile) throws CommitStepException { char[] password = myKeystorePasswordField.getPassword(); FileInputStream fis = null; - checkPassword(password); + AndroidUtils.checkPassword(password); if (!keystoreFile.isFile()) { throw new CommitStepException(AndroidBundle.message("android.cannot.find.file.error", keystoreFile.getPath())); } @@ -129,7 +130,7 @@ class KeystoreStep extends ExportSignedPackageWizardStep { } private void checkNewKeystoreOptions(File keystoreFile) throws CommitStepException { - checkNewPassword(myKeystorePasswordField, myConfirmKeystorePasswordField); + AndroidUtils.checkNewPassword(myKeystorePasswordField, myConfirmKeystorePasswordField); if (keystoreFile.exists()) { throw new CommitStepException(AndroidBundle.message( diff --git a/plugins/android/src/org/jetbrains/android/exportSignedPackage/NewKeyStep.form b/plugins/android/src/org/jetbrains/android/exportSignedPackage/NewKeyForm.form similarity index 99% rename from plugins/android/src/org/jetbrains/android/exportSignedPackage/NewKeyStep.form rename to plugins/android/src/org/jetbrains/android/exportSignedPackage/NewKeyForm.form index 74c03cda9970..a9cef39e58cb 100644 --- a/plugins/android/src/org/jetbrains/android/exportSignedPackage/NewKeyStep.form +++ b/plugins/android/src/org/jetbrains/android/exportSignedPackage/NewKeyForm.form @@ -1,5 +1,5 @@ -
+ @@ -71,7 +71,7 @@ - + diff --git a/plugins/android/src/org/jetbrains/android/exportSignedPackage/NewKeyForm.java b/plugins/android/src/org/jetbrains/android/exportSignedPackage/NewKeyForm.java new file mode 100644 index 000000000000..18558faf89f9 --- /dev/null +++ b/plugins/android/src/org/jetbrains/android/exportSignedPackage/NewKeyForm.java @@ -0,0 +1,270 @@ +package org.jetbrains.android.exportSignedPackage; + +import com.android.jarutils.DebugKeyProvider; +import com.android.jarutils.KeystoreHelper; +import com.intellij.ide.util.PropertiesComponent; +import com.intellij.ide.wizard.CommitStepException; +import com.intellij.openapi.diagnostic.Logger; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.ui.Messages; +import org.jetbrains.android.util.AndroidBundle; +import org.jetbrains.android.util.AndroidUtils; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import javax.swing.*; +import java.awt.*; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.security.KeyStore; +import java.security.PrivateKey; +import java.security.cert.Certificate; +import java.security.cert.X509Certificate; +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +/** + * @author Eugene.Kudelevsky + */ +public abstract class NewKeyForm { + private static final Logger LOG = Logger.getInstance("#org.jetbrains.android.exportSignedPackage.NewKeyForm"); + + private JPanel myContentPanel; + private JTextField myAliasField; + private JPasswordField myKeyPasswordField; + private JPasswordField myConfirmKeyPasswordField; + private JSpinner myValiditySpinner; + private JTextField myFirstAndLastNameField; + private JTextField myOrganizationUnitField; + private JTextField myCityField; + private JTextField myStateOrProvinceField; + private JTextField myCountryCodeField; + private JPanel myCertificatePanel; + private JTextField myOrganizationField; + + private KeyStore myKeyStore; + private PrivateKey myPrivateKey; + private X509Certificate myCertificate; + + public NewKeyForm() { + myValiditySpinner.setModel(new SpinnerNumberModel(25, 1, 1000, 1)); + } + + private int getValidity() { + SpinnerNumberModel model = (SpinnerNumberModel)myValiditySpinner.getModel(); + return model.getNumber().intValue(); + } + + public void init() { + myAliasField.setText(generateAlias()); + } + + public JPanel getContentPanel() { + return myContentPanel; + } + + private boolean findNonEmptyCertificateField() { + for (Component component : myCertificatePanel.getComponents()) { + if (component instanceof JTextField) { + if (((JTextField)component).getText().trim().length() > 0) { + return true; + } + } + } + return false; + } + + public void createKey() throws CommitStepException { + if (getKeyAlias().length() == 0) { + throw new CommitStepException(AndroidBundle.message("android.export.package.specify.key.alias.error")); + } + AndroidUtils.checkNewPassword(myKeyPasswordField, myConfirmKeyPasswordField); + if (!findNonEmptyCertificateField()) { + throw new CommitStepException(AndroidBundle.message("android.export.package.specify.certificate.field.error")); + } + doCreateKey(); + } + + @NotNull + private String generateAlias() { + List aliasList = getExistingKeyAliasList(); + String prefix = "key"; + if (aliasList == null) { + return prefix + '0'; + } + Set aliasSet = new HashSet(); + for (String alias : aliasList) { + aliasSet.add(alias.toLowerCase()); + } + for (int i = 0; ; i++) { + String alias = prefix + i; + if (!aliasSet.contains(alias)) { + return alias; + } + } + } + + @Nullable + protected abstract List getExistingKeyAliasList(); + + private static void buildDName(StringBuilder builder, String prefix, JTextField textField) { + if (textField != null) { + String value = textField.getText().trim(); + if (value.length() > 0) { + if (builder.length() > 0) { + builder.append(","); + } + builder.append(prefix); + builder.append('='); + builder.append(value); + } + } + } + + private String getDName() { + StringBuilder builder = new StringBuilder(); + buildDName(builder, "CN", myFirstAndLastNameField); + buildDName(builder, "OU", myOrganizationUnitField); + buildDName(builder, "O", myOrganizationField); + buildDName(builder, "L", myCityField); + buildDName(builder, "ST", myStateOrProvinceField); + buildDName(builder, "C", myCountryCodeField); + return builder.toString(); + } + + + + private void doCreateKey() throws CommitStepException { + String keystoreLocation = getKeyStoreLocation(); + char[] keystorePassword = getKeyStorePassword(); + char[] keyPassword = getKeyPassword(); + String keyAlias = getKeyAlias(); + String dname = getDName(); + assert dname != null; + boolean createdStore = false; + final StringBuilder errorBuilder = new StringBuilder(); + final StringBuilder outBuilder = new StringBuilder(); + try { + createdStore = KeystoreHelper + .createNewStore(keystoreLocation, null, new String(keystorePassword), keyAlias, new String(keyPassword), dname, getValidity(), + new DebugKeyProvider.IKeyGenOutput() { + public void err(String message) { + errorBuilder.append(message).append('\n'); + LOG.info("Error: " + message); + } + + public void out(String message) { + outBuilder.append(message).append('\n'); + LOG.info(message); + } + }); + } + catch (Exception e) { + LOG.info(e); + errorBuilder.append(e.getMessage()).append('\n'); + } + normalizeBuilder(errorBuilder); + normalizeBuilder(outBuilder); + try { + if (createdStore) { + if (errorBuilder.length() > 0) { + String prefix = AndroidBundle.message("android.create.new.key.error.prefix"); + Messages.showErrorDialog(myContentPanel, prefix + '\n' + errorBuilder.toString()); + } + } + else { + if (errorBuilder.length() > 0) { + throw new CommitStepException(errorBuilder.toString()); + } + if (outBuilder.length() > 0) { + throw new CommitStepException(outBuilder.toString()); + } + throw new CommitStepException(AndroidBundle.message("android.cannot.create.new.key.error")); + } + PropertiesComponent.getInstance(getProject()).setValue(KeystoreStep.DEFAULT_KEYSTORE_LOCATION, keystoreLocation); + loadKeystoreAndKey(keystoreLocation, keystorePassword, keyAlias, keyPassword); + } + finally { + Arrays.fill(keystorePassword, '\0'); + Arrays.fill(keyPassword, '\0'); + } + } + + @NotNull + public char[] getKeyPassword() { + return myKeyPasswordField.getPassword(); + } + + @NotNull + public String getKeyAlias() { + return myAliasField.getText().trim(); + } + + @NotNull + protected abstract Project getProject(); + + @NotNull + protected abstract char[] getKeyStorePassword(); + + @NotNull + protected abstract String getKeyStoreLocation(); + + private void loadKeystoreAndKey(String keystoreLocation, char[] keystorePassword, String keyAlias, char[] keyPassword) + throws CommitStepException { + FileInputStream fis = null; + try { + KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType()); + fis = new FileInputStream(new File(keystoreLocation)); + keyStore.load(fis, keystorePassword); + myKeyStore = keyStore; + KeyStore.PrivateKeyEntry entry = (KeyStore.PrivateKeyEntry)keyStore.getEntry(keyAlias, new KeyStore.PasswordProtection(keyPassword)); + if (entry == null) { + throw new CommitStepException(AndroidBundle.message("android.extract.package.cannot.find.key.error", keyAlias)); + } + PrivateKey privateKey = entry.getPrivateKey(); + Certificate certificate = entry.getCertificate(); + if (privateKey == null || certificate == null) { + throw new CommitStepException(AndroidBundle.message("android.extract.package.cannot.find.key.error", keyAlias)); + } + PropertiesComponent.getInstance(getProject()).setValue(InitialKeyStep.DEFAULT_KEY_ALIAS, keyAlias); + myPrivateKey = privateKey; + myCertificate = (X509Certificate)certificate; + } + catch (Exception e) { + throw new CommitStepException("Error: " + e.getMessage()); + } + finally { + if (fis != null) { + try { + fis.close(); + } + catch (IOException ignored) { + } + } + } + } + + private static void normalizeBuilder(StringBuilder builder) { + if (builder.length() > 0) { + builder.deleteCharAt(builder.length() - 1); + } + } + + @Nullable + public KeyStore getKeyStore() { + return myKeyStore; + } + + @Nullable + public PrivateKey getPrivateKey() { + return myPrivateKey; + } + + @Nullable + public X509Certificate getCertificate() { + return myCertificate; + } +} diff --git a/plugins/android/src/org/jetbrains/android/exportSignedPackage/NewKeyStep.java b/plugins/android/src/org/jetbrains/android/exportSignedPackage/NewKeyStep.java index bb4887969ae8..dcfb4d96f0fe 100644 --- a/plugins/android/src/org/jetbrains/android/exportSignedPackage/NewKeyStep.java +++ b/plugins/android/src/org/jetbrains/android/exportSignedPackage/NewKeyStep.java @@ -16,79 +16,37 @@ package org.jetbrains.android.exportSignedPackage; -import com.android.jarutils.DebugKeyProvider; -import com.android.jarutils.KeystoreHelper; -import com.intellij.ide.util.PropertiesComponent; import com.intellij.ide.wizard.CommitStepException; -import com.intellij.openapi.diagnostic.Logger; -import com.intellij.openapi.ui.Messages; -import org.jetbrains.android.util.AndroidBundle; +import com.intellij.openapi.project.Project; import org.jetbrains.annotations.NotNull; import javax.swing.*; -import java.awt.*; -import java.io.File; -import java.io.FileInputStream; -import java.io.IOException; import java.security.KeyStore; import java.security.PrivateKey; -import java.security.cert.Certificate; import java.security.cert.X509Certificate; -import java.util.Arrays; -import java.util.HashSet; import java.util.List; -import java.util.Set; /** * @author Eugene.Kudelevsky */ public class NewKeyStep extends ExportSignedPackageWizardStep { - private static final Logger LOG = Logger.getInstance("#com.jetbrains.android.exportSignedPackage.NewKeyStep"); - - private JPanel myContentPanel; - private JTextField myAliasField; - private JPasswordField myKeyPasswordField; - private JPasswordField myConfirmKeyPasswordField; - private JSpinner myValiditySpinner; - private JTextField myFirstAndLastNameField; - private JTextField myOrganizationUnitField; - private JTextField myCityField; - private JTextField myStateOrProvinceField; - private JTextField myCountryCodeField; - private JPanel myCertificatePanel; - private JTextField myOrganizationField; private final ExportSignedPackageWizard myWizard; + private final NewKeyForm myNewKeyForm; public NewKeyStep(ExportSignedPackageWizard wizard) { myWizard = wizard; - myValiditySpinner.setModel(new SpinnerNumberModel(25, 1, 1000, 1)); - } - - private int getValidity() { - SpinnerNumberModel model = (SpinnerNumberModel)myValiditySpinner.getModel(); - return model.getNumber().intValue(); + myNewKeyForm = new MyNewKeyForm(); } @Override public void _init() { - myAliasField.setText(generateAlias()); + myNewKeyForm.init(); } @Override public JComponent getComponent() { - return myContentPanel; - } - - private boolean findNonEmptyCertificateField() { - for (Component component : myCertificatePanel.getComponents()) { - if (component instanceof JTextField) { - if (((JTextField)component).getText().trim().length() > 0) { - return true; - } - } - } - return false; + return myNewKeyForm.getContentPanel(); } @Override @@ -98,157 +56,42 @@ public class NewKeyStep extends ExportSignedPackageWizardStep { @Override protected void commitForNext() throws CommitStepException { - if (myAliasField.getText().trim().length() == 0) { - throw new CommitStepException(AndroidBundle.message("android.export.package.specify.key.alias.error")); - } - checkNewPassword(myKeyPasswordField, myConfirmKeyPasswordField); - if (!findNonEmptyCertificateField()) { - throw new CommitStepException(AndroidBundle.message("android.export.package.specify.certificate.field.error")); - } - createKey(); + myNewKeyForm.createKey(); + final KeyStore keyStore = myNewKeyForm.getKeyStore(); + final PrivateKey privateKey = myNewKeyForm.getPrivateKey(); + final X509Certificate certificate = myNewKeyForm.getCertificate(); + assert keyStore != null && privateKey != null && certificate != null; + myWizard.setKeystore(keyStore); + myWizard.setPrivateKey(privateKey); + myWizard.setCertificate(certificate); } - @NotNull - private String generateAlias() { - List aliasList = myWizard.getKeyAliasList(); - String prefix = "key"; - if (aliasList == null) { - return prefix + '0'; + private class MyNewKeyForm extends NewKeyForm { + @Override + protected List getExistingKeyAliasList() { + return myWizard.getKeyAliasList(); } - Set aliasSet = new HashSet(); - for (String alias : aliasList) { - aliasSet.add(alias.toLowerCase()); - } - for (int i = 0; ; i++) { - String alias = prefix + i; - if (!aliasSet.contains(alias)) { - return alias; - } - } - } - private static void buildDName(StringBuilder builder, String prefix, JTextField textField) { - if (textField != null) { - String value = textField.getText().trim(); - if (value.length() > 0) { - if (builder.length() > 0) { - builder.append(","); - } - builder.append(prefix); - builder.append('='); - builder.append(value); - } + @NotNull + @Override + protected String getKeyStoreLocation() { + final String location = myWizard.getKeystoreLocation(); + assert location != null; + return location; } - } - private String getDName() { - StringBuilder builder = new StringBuilder(); - buildDName(builder, "CN", myFirstAndLastNameField); - buildDName(builder, "OU", myOrganizationUnitField); - buildDName(builder, "O", myOrganizationField); - buildDName(builder, "L", myCityField); - buildDName(builder, "ST", myStateOrProvinceField); - buildDName(builder, "C", myCountryCodeField); - return builder.toString(); - } + @NotNull + @Override + protected char[] getKeyStorePassword() { + final char[] password = myWizard.getKeystorePassword(); + assert password != null; + return password; + } - private void createKey() throws CommitStepException { - String keystoreLocation = myWizard.getKeystoreLocation(); - assert keystoreLocation != null; - char[] keystorePassword = myWizard.getKeystorePassword(); - assert keystorePassword != null; - char[] keyPassword = myKeyPasswordField.getPassword(); - String keyAlias = myAliasField.getText().trim(); - String dname = getDName(); - assert dname != null; - boolean createdStore = false; - final StringBuilder errorBuilder = new StringBuilder(); - final StringBuilder outBuilder = new StringBuilder(); - try { - createdStore = KeystoreHelper - .createNewStore(keystoreLocation, null, new String(keystorePassword), keyAlias, new String(keyPassword), dname, getValidity(), - new DebugKeyProvider.IKeyGenOutput() { - public void err(String message) { - errorBuilder.append(message).append('\n'); - LOG.info("Error: " + message); - } - - public void out(String message) { - outBuilder.append(message).append('\n'); - LOG.info(message); - } - }); - } - catch (Exception e) { - LOG.info(e); - errorBuilder.append(e.getMessage()).append('\n'); - } - normalizeBuilder(errorBuilder); - normalizeBuilder(outBuilder); - try { - if (createdStore) { - if (errorBuilder.length() > 0) { - String prefix = AndroidBundle.message("android.create.new.key.error.prefix"); - Messages.showErrorDialog(myContentPanel, prefix + '\n' + errorBuilder.toString()); - } - } - else { - if (errorBuilder.length() > 0) { - throw new CommitStepException(errorBuilder.toString()); - } - if (outBuilder.length() > 0) { - throw new CommitStepException(outBuilder.toString()); - } - throw new CommitStepException(AndroidBundle.message("android.cannot.create.new.key.error")); - } - PropertiesComponent.getInstance(myWizard.getProject()).setValue(KeystoreStep.DEFAULT_KEYSTORE_LOCATION, keystoreLocation); - loadKeystoreAndKey(keystoreLocation, keystorePassword, keyAlias, keyPassword); - } - finally { - Arrays.fill(keystorePassword, '\0'); - Arrays.fill(keyPassword, '\0'); - } - } - - private void loadKeystoreAndKey(String keystoreLocation, char[] keystorePassword, String keyAlias, char[] keyPassword) - throws CommitStepException { - FileInputStream fis = null; - try { - KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType()); - fis = new FileInputStream(new File(keystoreLocation)); - keyStore.load(fis, keystorePassword); - myWizard.setKeystore(keyStore); - KeyStore.PrivateKeyEntry entry = (KeyStore.PrivateKeyEntry)keyStore.getEntry(keyAlias, new KeyStore.PasswordProtection(keyPassword)); - if (entry == null) { - throw new CommitStepException(AndroidBundle.message("android.extract.package.cannot.find.key.error", keyAlias)); - } - PrivateKey privateKey = entry.getPrivateKey(); - Certificate certificate = entry.getCertificate(); - if (privateKey == null || certificate == null) { - throw new CommitStepException(AndroidBundle.message("android.extract.package.cannot.find.key.error", keyAlias)); - } - PropertiesComponent.getInstance(myWizard.getProject()).setValue(InitialKeyStep.DEFAULT_KEY_ALIAS, keyAlias); - myWizard.setPrivateKey(privateKey); - myWizard.setCertificate((X509Certificate)certificate); - - } - catch (Exception e) { - throw new CommitStepException("Error: " + e.getMessage()); - } - finally { - if (fis != null) { - try { - fis.close(); - } - catch (IOException ignored) { - } - } - } - } - - private static void normalizeBuilder(StringBuilder builder) { - if (builder.length() > 0) { - builder.deleteCharAt(builder.length() - 1); + @NotNull + @Override + protected Project getProject() { + return myWizard.getProject(); } } } diff --git a/plugins/android/src/org/jetbrains/android/util/AndroidUtils.java b/plugins/android/src/org/jetbrains/android/util/AndroidUtils.java index 2dc112b8b14d..9cf55702bbe1 100644 --- a/plugins/android/src/org/jetbrains/android/util/AndroidUtils.java +++ b/plugins/android/src/org/jetbrains/android/util/AndroidUtils.java @@ -37,6 +37,7 @@ import com.intellij.facet.FacetManager; import com.intellij.facet.ModifiableFacetModel; import com.intellij.facet.ProjectFacetManager; import com.intellij.ide.util.DefaultPsiElementCellRenderer; +import com.intellij.ide.wizard.CommitStepException; import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.module.Module; @@ -85,10 +86,7 @@ import org.jetbrains.annotations.Nullable; import javax.swing.*; import java.io.IOException; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.Set; +import java.util.*; /** * @author yole, coyote @@ -714,4 +712,47 @@ public class AndroidUtils { } return result; } + + public static void checkNewPassword(JPasswordField passwordField, JPasswordField confirmedPasswordField) throws CommitStepException { + char[] password = passwordField.getPassword(); + char[] confirmedPassword = confirmedPasswordField.getPassword(); + try { + checkPassword(password); + if (password.length < 6) { + throw new CommitStepException(AndroidBundle.message("android.export.package.incorrect.password.length")); + } + if (!Arrays.equals(password, confirmedPassword)) { + throw new CommitStepException(AndroidBundle.message("android.export.package.passwords.not.match.error")); + } + } + finally { + Arrays.fill(password, '\0'); + Arrays.fill(confirmedPassword, '\0'); + } + } + + public static void checkPassword(char[] password) throws CommitStepException { + if (password.length == 0) { + throw new CommitStepException(AndroidBundle.message("android.export.package.specify.password.error")); + } + } + + public static void checkPassword(JPasswordField passwordField) throws CommitStepException { + char[] password = passwordField.getPassword(); + try { + checkPassword(password); + } + finally { + Arrays.fill(password, '\0'); + } + } + + @NotNull + public static List toList(@NotNull Enumeration enumeration) { + final List result = new ArrayList(); + while (enumeration.hasMoreElements()) { + result.add(enumeration.nextElement()); + } + return result; + } }