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

io.bitrise.trace.plugin.modifier.ApplicationTransformHelper Maven / Gradle / Ivy

The newest version!
package io.bitrise.trace.plugin.modifier;

import androidx.annotation.NonNull;
import androidx.annotation.VisibleForTesting;
import com.android.build.api.transform.Format;
import com.android.build.api.transform.QualifiedContent;
import com.android.build.api.transform.TransformInvocation;
import com.android.build.gradle.BaseExtension;
import java.io.File;
import java.io.IOException;
import java.util.HashSet;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;
import javassist.CannotCompileException;
import javassist.CtClass;
import javassist.CtMethod;
import javassist.CtNewMethod;
import javassist.NotFoundException;
import javax.xml.parsers.ParserConfigurationException;
import org.gradle.api.Project;
import org.gradle.api.logging.Logger;
import org.xml.sax.SAXException;

/**
 * Helper class for doing the Application class related transforms in {@link TraceTransform}.
 */
public class ApplicationTransformHelper extends TransformHelper {

  ApplicationTransformHelper(@NonNull final Project project,
                             @NonNull final BaseExtension baseExtension,
                             @NonNull final Logger logger) {
    super(project, baseExtension, logger);
  }

  /**
   * Filters the {@code null} Strings from the given Set.
   *
   * @param strings the given Set.
   * @return the filtered Set.
   */
  @VisibleForTesting
  @NonNull
  static Set filterNullStrings(@NonNull final Set strings) {
    return strings.stream().filter(Objects::nonNull).collect(Collectors.toSet());
  }

  /**
   * Transform Application classes to initialise the Trace SDK. Searches the available
   * AndroidManifest.xml files for the Application class names and modifies them with Javassist.
   *
   * @param transformInvocation the {@link TransformInvocation}.
   * @param name                the unique name of the transform.
   * @param outputTypes         the type(s) of data that is generated by the Transform.
   * @param scopes              the scope(s) of the Transform. This indicates which scopes the
   *                            transform consumes.
   * @throws IOException                  if any I/O error occurs.
   * @throws SAXException                 if any parse errors occur during the parse of the
   *                                      AndroidManifest.xml files.
   * @throws ParserConfigurationException if a DocumentBuilder cannot be created which
   *                                      satisfies the configuration
   *                                      requested during the par«se of the AndroidManifest
   *                                      .xml files.
   * @throws NotFoundException            if the given class cannot be found in the ClassPool.
   * @throws CannotCompileException       if the updated code cannot compile.
   */
  void transformApplicationClasses(@NonNull final TransformInvocation transformInvocation,
                                   @NonNull final String name,
                                   @NonNull final Set outputTypes,
                                   @NonNull final Set scopes)
      throws IOException, SAXException, ParserConfigurationException, NotFoundException,
             CannotCompileException {
    final Set applicationNames = getApplicationNames(baseExtension);
    for (@NonNull final String applicationClassName : applicationNames) {
      final CtClass applicationClass = findClass(transformInvocation, applicationClassName);
      final File outputDirectory = transformInvocation.getOutputProvider()
                                                      .getContentLocation(name, outputTypes, scopes,
                                                          Format.DIRECTORY);
      modifyApplicationClass(applicationClass, outputDirectory);
    }
  }

  /**
   * Modifies the given Application class to call the initialisation of the Trace SDK. If
   * onCreate method is already present the init is called after it, otherwise adds the onCreate
   * to the class.
   *
   * @param applicationClass the {@link CtClass} of the Project's Application class.
   * @param outputDirectory  the output directory of the Application class.
   * @throws IOException            if an I/O error occurs, which is possible because the
   *                                construction of the
   *                                canonical pathname may require filesystem queries.
   * @throws CannotCompileException if the updated code cannot compile.
   * @throws IOException            if any I/O error occurs.
   * @throws NotFoundException      if the given class cannot be found in the ClassPool.
   */
  private void modifyApplicationClass(@NonNull final CtClass applicationClass,
                                      @NonNull final File outputDirectory)
      throws IOException, CannotCompileException, NotFoundException {
    final CtMethod onCreateMethod = applicationClass.getMethod("onCreate", "()V");
    if (onCreateMethod.getDeclaringClass().equals(applicationClass)) {
      updateOnCreate(applicationClass);
    } else {
      addOnCreate(applicationClass);
    }
    applicationClass.writeFile(outputDirectory.getCanonicalPath());
  }

  /**
   * Adds the onCreate method to the given class. The onCreate will initialise the Trace SDK.
   *
   * @param applicationClass the {@link CtClass} of the Application class of the Project.
   * @throws CannotCompileException if the updated code cannot compile.
   */
  private void addOnCreate(@NonNull final CtClass applicationClass) throws CannotCompileException {
    final String newOnCreateMethod =
        "   public void onCreate() {\n"
            + "       super.onCreate();\n"
            + "       io.bitrise.trace.TraceSdk.init(getApplicationContext());"
            + "   }";
    applicationClass.defrost();
    applicationClass.addMethod(CtNewMethod.make(newOnCreateMethod, applicationClass));
    applicationClass.freeze();
  }

  /**
   * Updates the onCreate method of the Application class to call the initialisation of the
   * Trace SDK.
   *
   * @param applicationClass the {@link CtClass} of the Application class of the Project.
   * @throws NotFoundException      when the onCreate method cannot be found.
   * @throws CannotCompileException if the updated code cannot compile.
   */
  private void updateOnCreate(@NonNull final CtClass applicationClass)
      throws NotFoundException, CannotCompileException {
    applicationClass.defrost();
    final CtMethod onCreateMethod = applicationClass.getMethod("onCreate", "()V");
    onCreateMethod.insertAfter("io.bitrise.trace.TraceSdk.init(getApplicationContext());");
    applicationClass.freeze();
  }

  /**
   * Gets the Application class names of the given application from the AndroidManifest.xml files.
   *
   * @param baseExtension the {@link BaseExtension} of the application.
   * @return the names of the Application classes.
   * @throws ParserConfigurationException if a DocumentBuilder cannot be created which satisfies
   *                                      the configuration requested.
   * @throws SAXException                 if any parse errors occur.
   * @throws IOException                  if any IO errors occur.
   */
  @NonNull
  private Set getApplicationNames(@NonNull final BaseExtension baseExtension)
      throws ParserConfigurationException, SAXException, IOException {
    final Set applicationNames = new HashSet<>();
    final Set androidManifests = getManifestPaths(baseExtension);
    for (@NonNull final String manifest : androidManifests) {
      final ManifestHelper manifestHelper;
      manifestHelper = new ManifestHelper(manifest, logger);
      applicationNames.add(makeFullyQualifiedName(manifestHelper.getApplicationName(),
          manifestHelper.getPackageName()));
    }
    return filterNullStrings(applicationNames);
  }

  /**
   * Gets the path of the AndroidManifest.xml files from all source sets for the given application.
   *
   * @param baseExtension the {@link BaseExtension} of the application.
   * @return the paths of the manifest files.
   */
  @NonNull
  private Set getManifestPaths(@NonNull final BaseExtension baseExtension) {
    final Set manifestPaths = new HashSet<>();

    baseExtension.getSourceSets().forEach(androidSourceSet -> {
      final File androidManifestFile = androidSourceSet.getManifest().getSrcFile();
      if (androidManifestFile.exists()) {
        manifestPaths.add(androidManifestFile.getPath());
      }
    });
    return manifestPaths;
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy