io.bitrise.trace.plugin.modifier.ApplicationTransformHelper Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of trace-gradle-plugin Show documentation
Show all versions of trace-gradle-plugin Show documentation
Catch bugs before they reach production.
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 super QualifiedContent.Scope> 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;
}
}