All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.jetbrains.android.compiler.tools.AndroidApkBuilder Maven / Gradle / Ivy

/*
 * Copyright 2000-2012 JetBrains s.r.o.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.jetbrains.android.compiler.tools;

import com.android.SdkConstants;
import com.android.jarutils.DebugKeyProvider;
import com.android.jarutils.JavaResourceFilter;
import com.android.jarutils.SignedJarBuilder;
import com.android.prefs.AndroidLocation;
import com.android.sdklib.IAndroidTarget;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.util.Condition;
import com.intellij.openapi.util.io.FileUtil;
import com.intellij.openapi.util.io.FileUtilRt;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.util.ArrayUtil;
import com.intellij.util.containers.HashMap;
import com.intellij.util.containers.HashSet;
import com.intellij.util.text.DateFormatUtil;
import org.jetbrains.android.util.*;
import org.jetbrains.annotations.NonNls;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.security.*;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.*;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;

import static org.jetbrains.android.util.AndroidCompilerMessageKind.*;

/**
 * @author yole
 */
public class AndroidApkBuilder {
  private static final Logger LOG = Logger.getInstance("#org.jetbrains.android.compiler.tools.AndroidApkBuilder");

  @NonNls private static final String UNALIGNED_SUFFIX = ".unaligned";
  @NonNls private static final String EXT_NATIVE_LIB = "so";

  private AndroidApkBuilder() {
  }

  private static Map> filterUsingKeystoreMessages(Map> messages) {
    List infoMessages = messages.get(INFORMATION);
    if (infoMessages == null) {
      infoMessages = new ArrayList();
      messages.put(INFORMATION, infoMessages);
    }
    final List errors = messages.get(ERROR);
    for (Iterator iterator = errors.iterator(); iterator.hasNext();) {
      String s = iterator.next();
      if (s.startsWith("Using keystore:")) {
        // not actually an error
        infoMessages.add(s);
        iterator.remove();
      }
    }
    return messages;
  }

  @SuppressWarnings({"IOResourceOpenedButNotSafelyClosed"})
  private static void collectDuplicateEntries(@NotNull String rootFile, @NotNull Set entries, @NotNull Set result)
    throws IOException {
    final JavaResourceFilter javaResourceFilter = new JavaResourceFilter();

    FileInputStream fis = null;
    ZipInputStream zis = null;
    try {
      fis = new FileInputStream(rootFile);
      zis = new ZipInputStream(fis);

      ZipEntry entry;
      while ((entry = zis.getNextEntry()) != null) {
        if (!entry.isDirectory()) {
          String name = entry.getName();
          if (javaResourceFilter.checkEntry(name) && !entries.add(name)) {
            result.add(name);
          }
          zis.closeEntry();
        }
      }
    }
    finally {
      if (zis != null) {
        zis.close();
      }
      if (fis != null) {
        fis.close();
      }
    }
  }

  public static Map> execute(@NotNull String resPackagePath,
                                                                      @NotNull String dexPath,
                                                                      @NotNull String[] resourceRoots,
                                                                      @NotNull String[] externalJars,
                                                                      @NotNull String[] nativeLibsFolders,
                                                                      @NotNull Collection additionalNativeLibs,
                                                                      @NotNull String finalApk,
                                                                      boolean unsigned,
                                                                      @NotNull String sdkPath,
                                                                      @NotNull IAndroidTarget target,
                                                                      @Nullable String customKeystorePath,
                                                                      @NotNull Condition resourceFilter) throws IOException {
    final AndroidBuildTestingManager testingManager = AndroidBuildTestingManager.getTestingManager();

    if (testingManager != null) {
      testingManager.getCommandExecutor().log(StringUtil.join(new String[]{
        "apk_builder",
        resPackagePath,
        dexPath,
        AndroidBuildTestingManager.arrayToString(resourceRoots),
        AndroidBuildTestingManager.arrayToString(externalJars),
        AndroidBuildTestingManager.arrayToString(nativeLibsFolders),
        additionalNativeLibs.toString(),
        finalApk,
        Boolean.toString(unsigned),
        sdkPath,
        customKeystorePath}, "\n"));
    }
    final Map> map = new HashMap>();
    map.put(ERROR, new ArrayList());
    map.put(WARNING, new ArrayList());

    final File outputDir = new File(finalApk).getParentFile();
    if (!outputDir.exists() && !outputDir.mkdirs()) {
      map.get(ERROR).add("Cannot create directory " + outputDir.getPath());
      return map;
    }
    File additionalLibsDir = null;
    try {
      if (additionalNativeLibs.size() > 0) {
        additionalLibsDir = FileUtil.createTempDirectory("android_additional_libs", "tmp");

        if (!copyNativeLibs(additionalNativeLibs, additionalLibsDir, map)) {
          return map;
        }
        nativeLibsFolders = ArrayUtil.append(nativeLibsFolders, additionalLibsDir.getPath());
      }

      if (unsigned) {
        return filterUsingKeystoreMessages(
          finalPackage(dexPath, resourceRoots, externalJars, nativeLibsFolders, finalApk, resPackagePath, customKeystorePath, false,
                       resourceFilter));
      }
      final String zipAlignPath = AndroidCommonUtils.getZipAlign(sdkPath, target);
      boolean withAlignment = new File(zipAlignPath).exists();
      String unalignedApk = AndroidCommonUtils.addSuffixToFileName(finalApk, UNALIGNED_SUFFIX);

      Map> map2 = filterUsingKeystoreMessages(
        finalPackage(dexPath, resourceRoots, externalJars, nativeLibsFolders, withAlignment ? unalignedApk : finalApk, resPackagePath,
                     customKeystorePath, true, resourceFilter));
      map.putAll(map2);

      if (withAlignment && map.get(ERROR).size() == 0) {
        map2 = AndroidExecutionUtil.doExecute(zipAlignPath, "-f", "4", unalignedApk, finalApk);
        map.putAll(map2);
      }
      return map;
    }
    finally {
      if (additionalLibsDir != null) {
        FileUtil.delete(additionalLibsDir);
      }
    }
  }

  private static boolean copyNativeLibs(@NotNull Collection libs,
                                        @NotNull File targetDir,
                                        @NotNull Map> map) throws IOException {
    for (AndroidNativeLibData lib : libs) {
      final String path = lib.getPath();
      final File srcFile = new File(path);
      if (!srcFile.exists()) {
        map.get(WARNING).add("File not found: " + FileUtil.toSystemDependentName(path) + ". The native library won't be placed into APK");
        continue;
      }
      final File dstDir = new File(targetDir, lib.getArchitecture());

      final File dstFile = new File(dstDir, lib.getTargetFileName());
      if (dstFile.exists()) {
        map.get(WARNING).add("Duplicate native library " + dstFile.getName() + "; " + dstFile.getPath() + " already exists");
        continue;
      }

      if (!dstDir.exists() && !dstDir.mkdirs()) {
        map.get(ERROR).add("Cannot create directory: " + FileUtil.toSystemDependentName(dstDir.getPath()));
        continue;
      }
      FileUtil.copy(srcFile, dstFile);
    }
    return map.get(ERROR).size() == 0;
  }

  private static Map> finalPackage(@NotNull String dexPath,
                                                                            @NotNull String[] javaResourceRoots,
                                                                            @NotNull String[] externalJars,
                                                                            @NotNull String[] nativeLibsFolders,
                                                                            @NotNull String outputApk,
                                                                            @NotNull String apkPath,
                                                                            @Nullable String customKeystorePath,
                                                                            boolean signed,
                                                                            @NotNull Condition resourceFilter) {
    final Map> result = new HashMap>();
    result.put(ERROR, new ArrayList());
    result.put(INFORMATION, new ArrayList());
    result.put(WARNING, new ArrayList());

    FileOutputStream fos = null;
    SignedJarBuilder builder = null;
    try {

      String keyStoreOsPath = customKeystorePath != null && customKeystorePath.length() > 0
                              ? customKeystorePath 
                              : DebugKeyProvider.getDefaultKeyStoreOsPath();
      
      DebugKeyProvider provider = createDebugKeyProvider(result, keyStoreOsPath);

      X509Certificate certificate = signed ? (X509Certificate)provider.getCertificate() : null;

      if (certificate != null && certificate.getNotAfter().compareTo(new Date()) < 0) {
        // generate a new one
        File keyStoreFile = new File(keyStoreOsPath);
        if (keyStoreFile.exists()) {
          keyStoreFile.delete();
        }
        provider = createDebugKeyProvider(result, keyStoreOsPath);
        certificate = (X509Certificate)provider.getCertificate();
      }

      if (certificate != null && certificate.getNotAfter().compareTo(new Date()) < 0) {
        String date = DateFormatUtil.formatPrettyDateTime(certificate.getNotAfter());
        result.get(ERROR).add(
          ("Debug certificate expired on " + date + ". Cannot regenerate it, please delete file \"" + keyStoreOsPath + "\" manually."));
        return result;
      }

      PrivateKey key = provider.getDebugKey();

      if (key == null) {
        result.get(ERROR).add("Cannot create new key or keystore");
        return result;
      }

      if (!new File(apkPath).exists()) {
        result.get(ERROR).add("File " + apkPath + " not found. Try to rebuild project");
        return result;
      }

      File dexEntryFile = new File(dexPath);
      if (!dexEntryFile.exists()) {
        result.get(ERROR).add("File " + dexEntryFile.getPath() + " not found. Try to rebuild project");
        return result;
      }

      for (String externalJar : externalJars) {
        if (new File(externalJar).isDirectory()) {
          result.get(ERROR).add(externalJar + " is directory. Directory libraries are not supported");
        }
      }

      if (result.get(ERROR).size() > 0) {
        return result;
      }

      fos = new FileOutputStream(outputApk);
      builder = new SafeSignedJarBuilder(fos, key, certificate, outputApk);

      FileInputStream fis = new FileInputStream(apkPath);
      try {
        builder.writeZip(fis, null);
      }
      finally {
        fis.close();
      }

      builder.writeFile(dexEntryFile, AndroidCommonUtils.CLASSES_FILE_NAME);

      final HashSet added = new HashSet();
      for (String resourceRootPath : javaResourceRoots) {
        final HashSet javaResources = new HashSet();
        final File resourceRoot = new File(resourceRootPath);
        collectStandardJavaResources(resourceRoot, javaResources, resourceFilter);
        writeStandardJavaResources(javaResources, resourceRoot, builder, added);
      }

      Set duplicates = new HashSet();
      Set entries = new HashSet();
      for (String externalJar : externalJars) {
        collectDuplicateEntries(externalJar, entries, duplicates);
      }

      for (String duplicate : duplicates) {
        result.get(WARNING).add("Duplicate entry " + duplicate + ". The file won't be added");
      }

      MyResourceFilter filter = new MyResourceFilter(duplicates);

      for (String externalJar : externalJars) {
        fis = new FileInputStream(externalJar);
        try {
          builder.writeZip(fis, filter);
        }
        finally {
          fis.close();
        }
      }

      final HashSet nativeLibs = new HashSet();
      for (String nativeLibsFolderPath : nativeLibsFolders) {
        final File nativeLibsFolder = new File(nativeLibsFolderPath);
        final File[] children = nativeLibsFolder.listFiles();

        if (children != null) {
          for (File child : children) {
            writeNativeLibraries(builder, nativeLibsFolder, child, signed, nativeLibs);
          }
        }
      }
    }
    catch (IOException e) {
      return addExceptionMessage(e, result);
    }
    catch (CertificateException e) {
      return addExceptionMessage(e, result);
    }
    catch (DebugKeyProvider.KeytoolException e) {
      return addExceptionMessage(e, result);
    }
    catch (AndroidLocation.AndroidLocationException e) {
      return addExceptionMessage(e, result);
    }
    catch (NoSuchAlgorithmException e) {
      return addExceptionMessage(e, result);
    }
    catch (UnrecoverableEntryException e) {
      return addExceptionMessage(e, result);
    }
    catch (KeyStoreException e) {
      return addExceptionMessage(e, result);
    }
    catch (GeneralSecurityException e) {
      return addExceptionMessage(e, result);
    }
    finally {
      if (builder != null) {
        try {
          builder.close();
        }
        catch (IOException e) {
          addExceptionMessage(e, result);
        }
        catch (GeneralSecurityException e) {
          addExceptionMessage(e, result);
        }
      }

      if (fos != null) {
        try {
          fos.close();
        }
        catch (IOException ignored) {
        }
      }
    }
    return result;
  }

  private static DebugKeyProvider createDebugKeyProvider(final Map> result, String path) throws
                                                                                                                               KeyStoreException,
                                                                                                                               NoSuchAlgorithmException,
                                                                                                                               CertificateException,
                                                                                                                               UnrecoverableEntryException,
                                                                                                                               IOException,
                                                                                                                               DebugKeyProvider.KeytoolException,
                                                                                                                               AndroidLocation.AndroidLocationException {

    return new DebugKeyProvider(path, null, new DebugKeyProvider.IKeyGenOutput() {
      public void err(String message) {
        result.get(ERROR).add("Error during key creation: " + message);
      }

      public void out(String message) {
        result.get(INFORMATION).add("Info message during key creation: " + message);
      }
    });
  }

  private static void writeNativeLibraries(SignedJarBuilder builder,
                                           File nativeLibsFolder,
                                           File child,
                                           boolean debugBuild,
                                           Set added)
    throws IOException {
    ArrayList list = new ArrayList();
    collectNativeLibraries(child, list, debugBuild);

    for (File file : list) {
      final String relativePath = FileUtil.getRelativePath(nativeLibsFolder, file);
      String path = FileUtil.toSystemIndependentName(SdkConstants.FD_APK_NATIVE_LIBS + File.separator + relativePath);

      if (added.add(path)) {
        builder.writeFile(file, path);
        LOG.info("Native lib file added to APK: " + file.getPath());
      }
      else {
        LOG.info("Duplicate in APK: native lib file " + file.getPath() + " won't be added.");
      }
    }
  }

  private static Map> addExceptionMessage(Exception e,
                                                                                Map> result) {
    LOG.info(e);
    String simpleExceptionName = e.getClass().getCanonicalName();
    result.get(ERROR).add(simpleExceptionName + ": " + e.getMessage());
    return result;
  }

  public static void collectNativeLibraries(@NotNull File file, @NotNull List result, boolean debugBuild) {
    if (!file.isDirectory()) {

      // some users store jars and *.so libs in the same directory. Do not pack JARs to APK's "lib" folder!
      if (FileUtilRt.extensionEquals(file.getName(), EXT_NATIVE_LIB) ||
          (debugBuild && !(FileUtilRt.extensionEquals(file.getName(), "jar")))) {
        result.add(file);
      }
    }
    else if (JavaResourceFilter.checkFolderForPackaging(file.getName())) {
      final File[] children = file.listFiles();

      if (children != null) {
        for (File child : children) {
          collectNativeLibraries(child, result, debugBuild);
        }
      }
    }
  }

  public static void collectStandardJavaResources(@NotNull File folder,
                                                  @NotNull Collection result,
                                                  @NotNull Condition filter) {
    final File[] children = folder.listFiles();

    if (children != null) {
      for (File child : children) {
        if (child.exists()) {
          if (child.isDirectory()) {
            if (JavaResourceFilter.checkFolderForPackaging(child.getName()) && filter.value(child)) {
              collectStandardJavaResources(child, result, filter);
            }
          }
          else if (checkFileForPackaging(child) && filter.value(child)) {
            result.add(child);
          }
        }
      }
    }
  }

  private static void writeStandardJavaResources(Collection resources,
                                                 File sourceRoot,
                                                 SignedJarBuilder jarBuilder,
                                                 Set added) throws IOException {
    for (File child : resources) {
      final String relativePath = FileUtil.getRelativePath(sourceRoot, child);
      if (relativePath != null && !added.contains(relativePath)) {
        jarBuilder.writeFile(child, FileUtil.toSystemIndependentName(relativePath));
        added.add(relativePath);
      }
    }
  }

  public static boolean checkFileForPackaging(@NotNull File file) {
    String fileName = FileUtil.getNameWithoutExtension(file);
    if (fileName.length() > 0) {
      final String extension = FileUtilRt.getExtension(file.getName());

      if (SdkConstants.EXT_ANDROID_PACKAGE.equals(extension)) {
        return false;
      }
      return JavaResourceFilter.checkFileForPackaging(fileName, extension);
    }
    return false;
  }

  private static class MyResourceFilter extends JavaResourceFilter {
    private final Set myExcludedEntries;

    public MyResourceFilter(@NotNull Set excludedEntries) {
      myExcludedEntries = excludedEntries;
    }

    @Override
    public boolean checkEntry(String name) {
      if (myExcludedEntries.contains(name)) {
        return false;
      }
      return super.checkEntry(name);
    }
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy