fix(gradle): add legacy support for vector drawables (PR #1879)

This commit is contained in:
nitram84
2023-05-25 21:19:26 +02:00
committed by GitHub
parent 8a67c39279
commit ea5b66d463
14 changed files with 270 additions and 60 deletions
@@ -42,7 +42,7 @@ import jadx.core.dex.nodes.MethodNode;
import jadx.core.dex.nodes.PackageNode;
import jadx.core.dex.nodes.RootNode;
import jadx.core.dex.visitors.SaveCode;
import jadx.core.export.ExportGradleProject;
import jadx.core.export.ExportGradleTask;
import jadx.core.plugins.JadxPluginManager;
import jadx.core.utils.DecompilerScheduler;
import jadx.core.utils.Utils;
@@ -50,7 +50,6 @@ import jadx.core.utils.exceptions.JadxRuntimeException;
import jadx.core.utils.files.FileUtils;
import jadx.core.xmlgen.BinaryXMLParser;
import jadx.core.xmlgen.ProtoXMLParser;
import jadx.core.xmlgen.ResContainer;
import jadx.core.xmlgen.ResourcesSaver;
/**
@@ -281,50 +280,42 @@ public final class JadxDecompiler implements Closeable {
}
File sourcesOutDir;
File resOutDir;
List<Runnable> tasks = new ArrayList<>();
TaskBarrier barrier = new TaskBarrier();
if (args.isExportAsGradleProject()) {
ResourceFile androidManifest = resources.stream()
.filter(resourceFile -> resourceFile.getType() == ResourceType.MANIFEST)
.findFirst()
.orElseThrow(IllegalStateException::new);
ResContainer strings = resources.stream()
.filter(resourceFile -> resourceFile.getType() == ResourceType.ARSC)
.findFirst()
.orElseThrow(IllegalStateException::new)
.loadContent()
.getSubFiles()
.stream()
.filter(resContainer -> resContainer.getFileName().contains("strings.xml"))
.findFirst()
.orElseThrow(IllegalStateException::new);
ExportGradleProject export = new ExportGradleProject(root, args.getOutDir(), androidManifest, strings);
export.init();
sourcesOutDir = export.getSrcOutDir();
resOutDir = export.getResOutDir();
ExportGradleTask gradleExportTask = new ExportGradleTask(resources, root, args.getOutDir(), barrier);
gradleExportTask.init();
sourcesOutDir = gradleExportTask.getSrcOutDir();
resOutDir = gradleExportTask.getResOutDir();
tasks.add(gradleExportTask);
} else {
sourcesOutDir = args.getOutDirSrc();
resOutDir = args.getOutDirRes();
}
List<Runnable> tasks = new ArrayList<>();
int taskCount = 0;
// save resources first because decompilation can hang or fail
if (saveResources) {
appendResourcesSaveTasks(tasks, resOutDir);
taskCount = appendResourcesSaveTasks(tasks, resOutDir, barrier);
}
if (saveSources) {
appendSourcesSave(tasks, sourcesOutDir);
taskCount += appendSourcesSave(tasks, sourcesOutDir, barrier);
}
barrier.setUpBarrier(taskCount);
return tasks;
}
private void appendResourcesSaveTasks(List<Runnable> tasks, File outDir) {
private int appendResourcesSaveTasks(List<Runnable> tasks, File outDir, TaskBarrier barrier) {
int numResourceTasks = 0;
if (args.isSkipFilesSave()) {
return;
return 0;
}
// process AndroidManifest.xml first to load complete resource ids table
for (ResourceFile resourceFile : getResources()) {
if (resourceFile.getType() == ResourceType.MANIFEST) {
new ResourcesSaver(outDir, resourceFile).run();
break;
}
}
@@ -340,11 +331,14 @@ public final class JadxDecompiler implements Closeable {
// ignore resource made from input file
continue;
}
tasks.add(new ResourcesSaver(outDir, resourceFile));
tasks.add(new ResourcesSaver(outDir, resourceFile, barrier));
numResourceTasks++;
}
return numResourceTasks;
}
private void appendSourcesSave(List<Runnable> tasks, File outDir) {
private int appendSourcesSave(List<Runnable> tasks, File outDir, TaskBarrier barrier) {
int numSourceTasks = 0;
Predicate<String> classFilter = args.getClassFilter();
List<JavaClass> classes = getClasses();
List<JavaClass> processQueue = new ArrayList<>(classes.size());
@@ -369,17 +363,25 @@ public final class JadxDecompiler implements Closeable {
}
for (List<JavaClass> decompileBatch : batches) {
tasks.add(() -> {
for (JavaClass cls : decompileBatch) {
try {
ClassNode clsNode = cls.getClassNode();
ICodeInfo code = clsNode.getCode();
SaveCode.save(outDir, clsNode, code);
} catch (Exception e) {
LOG.error("Error saving class: {}", cls, e);
try {
for (JavaClass cls : decompileBatch) {
try {
ClassNode clsNode = cls.getClassNode();
ICodeInfo code = clsNode.getCode();
SaveCode.save(outDir, clsNode, code);
} catch (Exception e) {
LOG.error("Error saving class: {}", cls, e);
}
}
} finally {
if (barrier != null) {
barrier.finishTask();
}
}
});
numSourceTasks++;
}
return numSourceTasks;
}
public List<JavaClass> getClasses() {
@@ -0,0 +1,22 @@
package jadx.api;
import java.util.concurrent.CountDownLatch;
public class TaskBarrier {
private CountDownLatch taskCountDown = null;
public void setUpBarrier(final int numTasks) {
taskCountDown = new CountDownLatch(numTasks);
}
public CountDownLatch getTaskCountDown() {
return taskCountDown;
}
public void finishTask() {
if (taskCountDown != null) {
taskCountDown.countDown();
}
}
}
@@ -46,6 +46,7 @@ import jadx.core.dex.visitors.DepthTraversal;
import jadx.core.dex.visitors.IDexTreeVisitor;
import jadx.core.dex.visitors.typeinference.TypeCompare;
import jadx.core.dex.visitors.typeinference.TypeUpdate;
import jadx.core.export.GradleInfoStorage;
import jadx.core.utils.CacheStorage;
import jadx.core.utils.ErrorsCounter;
import jadx.core.utils.PassMerge;
@@ -78,6 +79,8 @@ public class RootNode {
private final TypeUtils typeUtils;
private final AttributeStorage attributes = new AttributeStorage();
private final GradleInfoStorage gradleInfoStorage = new GradleInfoStorage();
private final Map<ClassInfo, ClassNode> clsMap = new HashMap<>();
private final Map<String, ClassNode> rawClsMap = new HashMap<>();
private List<ClassNode> classes = new ArrayList<>();
@@ -712,4 +715,8 @@ public class RootNode {
public boolean isProto() {
return isProto;
}
public GradleInfoStorage getGradleInfoStorage() {
return gradleInfoStorage;
}
}
@@ -3,6 +3,8 @@ package jadx.core.export;
import java.io.File;
import java.io.IOException;
import java.io.StringReader;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Pattern;
import javax.xml.parsers.DocumentBuilder;
@@ -15,7 +17,6 @@ import org.xml.sax.InputSource;
import jadx.api.ResourceFile;
import jadx.core.dex.nodes.RootNode;
import jadx.core.utils.exceptions.JadxRuntimeException;
import jadx.core.utils.files.FileUtils;
import jadx.core.xmlgen.ResContainer;
import jadx.core.xmlgen.XmlSecurity;
@@ -25,25 +26,19 @@ public class ExportGradleProject {
private final RootNode root;
private final File projectDir;
private final File appDir;
private final File srcOutDir;
private final File resOutDir;
private final ApplicationParams applicationParams;
public ExportGradleProject(RootNode root, File projectDir, ResourceFile androidManifest, ResContainer appStrings) {
this.root = root;
this.projectDir = projectDir;
this.appDir = new File(projectDir, "app");
this.srcOutDir = new File(appDir, "src/main/java");
this.resOutDir = new File(appDir, "src/main");
this.applicationParams = getApplicationParams(
parseAndroidManifest(androidManifest),
parseAppStrings(appStrings));
}
public void init() {
public void generateGradleFiles() {
try {
FileUtils.makeDirs(srcOutDir);
FileUtils.makeDirs(resOutDir);
saveProjectBuildGradle();
saveApplicationBuildGradle();
saveSettingsGradle();
@@ -72,14 +67,32 @@ public class ExportGradleProject {
appPackage = "UNKNOWN";
}
Integer minSdkVersion = applicationParams.getMinSdkVersion();
tmpl.add("applicationId", appPackage);
tmpl.add("minSdkVersion", applicationParams.getMinSdkVersion());
tmpl.add("minSdkVersion", minSdkVersion);
tmpl.add("targetSdkVersion", applicationParams.getTargetSdkVersion());
tmpl.add("versionCode", applicationParams.getVersionCode());
tmpl.add("versionName", applicationParams.getVersionName());
List<String> additionalOptions = new ArrayList<>();
GradleInfoStorage gradleInfo = root.getGradleInfoStorage();
if (gradleInfo.isVectorPathData() && minSdkVersion < 21 || gradleInfo.isVectorFillType() && minSdkVersion < 24) {
additionalOptions.add("vectorDrawables.useSupportLibrary = true");
}
genAdditionalAndroidPluginOptions(tmpl, additionalOptions);
tmpl.save(new File(appDir, "build.gradle"));
}
private void genAdditionalAndroidPluginOptions(TemplateFile tmpl, List<String> additionalOptions) {
StringBuilder sb = new StringBuilder();
for (String additionalOption : additionalOptions) {
sb.append(" ").append(additionalOption).append('\n');
}
tmpl.add("additionalOptions", sb.toString());
}
private ApplicationParams getApplicationParams(Document androidManifest, Document appStrings) {
Element manifest = (Element) androidManifest.getElementsByTagName("manifest").item(0);
Element usesSdk = (Element) androidManifest.getElementsByTagName("uses-sdk").item(0);
@@ -140,12 +153,4 @@ public class ExportGradleProject {
return parseXml(content);
}
public File getSrcOutDir() {
return srcOutDir;
}
public File getResOutDir() {
return resOutDir;
}
}
@@ -0,0 +1,79 @@
package jadx.core.export;
import java.io.File;
import java.util.List;
import jadx.api.ResourceFile;
import jadx.api.ResourceType;
import jadx.api.TaskBarrier;
import jadx.core.dex.nodes.RootNode;
import jadx.core.utils.exceptions.JadxRuntimeException;
import jadx.core.utils.files.FileUtils;
import jadx.core.xmlgen.ResContainer;
public class ExportGradleTask implements Runnable {
private final List<ResourceFile> resources;
private final RootNode root;
private final File projectDir;
private final File appDir;
private final File srcOutDir;
private final File resOutDir;
private final TaskBarrier barrier;
public ExportGradleTask(List<ResourceFile> resources, RootNode root, File projectDir, TaskBarrier barrier) {
this.resources = resources;
this.projectDir = projectDir;
this.root = root;
this.appDir = new File(projectDir, "app");
this.srcOutDir = new File(appDir, "src/main/java");
this.resOutDir = new File(appDir, "src/main");
this.barrier = barrier;
}
@Override
public void run() {
ResourceFile androidManifest = resources.stream()
.filter(resourceFile -> resourceFile.getType() == ResourceType.MANIFEST)
.findFirst()
.orElseThrow(IllegalStateException::new);
ResContainer strings = resources.stream()
.filter(resourceFile -> resourceFile.getType() == ResourceType.ARSC)
.findFirst()
.orElseThrow(IllegalStateException::new)
.loadContent()
.getSubFiles()
.stream()
.filter(resContainer -> resContainer.getFileName().contains("strings.xml"))
.findFirst()
.orElseThrow(IllegalStateException::new);
ExportGradleProject export = new ExportGradleProject(root, projectDir, androidManifest, strings);
// wait until all sources and resources are exported and all necessary info for gradle export are
// collected
try {
barrier.getTaskCountDown().await();
} catch (InterruptedException e) {
throw new JadxRuntimeException("Gradle export failed", e);
}
export.generateGradleFiles();
}
public void init() {
FileUtils.makeDirs(srcOutDir);
FileUtils.makeDirs(resOutDir);
}
public File getSrcOutDir() {
return srcOutDir;
}
public File getResOutDir() {
return resOutDir;
}
}
@@ -0,0 +1,24 @@
package jadx.core.export;
public class GradleInfoStorage {
private boolean vectorPathData;
private boolean vectorFillType;
public boolean isVectorPathData() {
return vectorPathData;
}
public void setVectorPathData(boolean vectorPathData) {
this.vectorPathData = vectorPathData;
}
public boolean isVectorFillType() {
return vectorFillType;
}
public void setVectorFillType(boolean vectorFillType) {
this.vectorFillType = vectorFillType;
}
}
@@ -316,6 +316,13 @@ public class BinaryXMLParser extends CommonBinaryParser {
decodeAttribute(attributeNS, attrValDataType, attrValData,
shortNsName, attrName);
}
if (shortNsName != null && shortNsName.equals("android")) {
if (attrName.equals("pathData")) {
rootNode.getGradleInfoStorage().setVectorPathData(true);
} else if (attrName.equals("fillType")) {
rootNode.getGradleInfoStorage().setVectorFillType(true);
}
}
writer.add('"');
}
@@ -10,6 +10,7 @@ import org.slf4j.LoggerFactory;
import jadx.api.ResourceFile;
import jadx.api.ResourcesLoader;
import jadx.api.TaskBarrier;
import jadx.api.plugins.utils.ZipSecurity;
import jadx.core.dex.visitors.SaveCode;
import jadx.core.utils.exceptions.JadxException;
@@ -22,17 +23,29 @@ public class ResourcesSaver implements Runnable {
private final ResourceFile resourceFile;
private final File outDir;
private TaskBarrier barrier = null;
public ResourcesSaver(File outDir, ResourceFile resourceFile) {
this.resourceFile = resourceFile;
this.outDir = outDir;
}
public ResourcesSaver(File outDir, ResourceFile resourceFile, TaskBarrier barrier) {
this.resourceFile = resourceFile;
this.outDir = outDir;
this.barrier = barrier;
}
@Override
public void run() {
try {
saveResources(resourceFile.loadContent());
} catch (Throwable e) {
LOG.warn("Failed to save resource: {}", resourceFile.getOriginalName(), e);
} finally {
if (barrier != null) {
barrier.finishTask();
}
}
}
@@ -12,7 +12,7 @@ android {
targetSdkVersion {{targetSdkVersion}}
versionCode {{versionCode}}
versionName "{{versionName}}"
{{additionalOptions}}
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
@@ -4,6 +4,7 @@ import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.util.List;
import java.util.stream.Stream;
import org.junit.jupiter.api.io.TempDir;
@@ -12,8 +13,10 @@ import jadx.api.ICodeInfo;
import jadx.api.JadxDecompiler;
import jadx.api.JadxDecompilerTestUtils;
import jadx.api.ResourceFile;
import jadx.api.TaskBarrier;
import jadx.core.dex.nodes.RootNode;
import jadx.core.export.ExportGradleProject;
import jadx.core.export.ExportGradleTask;
import jadx.core.xmlgen.ResContainer;
import static org.assertj.core.api.Assertions.assertThat;
@@ -23,6 +26,13 @@ import static org.mockito.Mockito.when;
public abstract class ExportGradleTest {
private final RootNode root;
public ExportGradleTest() {
final JadxDecompiler decompiler = JadxDecompilerTestUtils.getMockDecompiler();
root = decompiler.getRoot();
}
private static final String MANIFEST_TESTS_DIR = "src/test/manifest";
@TempDir
@@ -47,19 +57,25 @@ public abstract class ExportGradleTest {
return contentBuilder.toString();
}
protected RootNode getRootNode() {
return root;
}
protected void exportGradle(String manifestFilename, String stringsFileName) {
final JadxDecompiler decompiler = JadxDecompilerTestUtils.getMockDecompiler();
ResourceFile androidManifest = mock(ResourceFile.class);
final ResContainer androidManifestContainer = createResourceContainer(manifestFilename);
when(androidManifest.loadContent()).thenReturn(androidManifestContainer);
final ResContainer strings = createResourceContainer(stringsFileName);
final RootNode root = decompiler.getRoot();
TaskBarrier taskBarrier = new TaskBarrier();
final ExportGradleTask exportGradleTask = new ExportGradleTask(List.of(androidManifest), root, exportDir, taskBarrier);
exportGradleTask.init();
assertThat(exportGradleTask.getSrcOutDir().exists());
assertThat(exportGradleTask.getResOutDir().exists());
final ExportGradleProject export =
new ExportGradleProject(root, exportDir, androidManifest, strings);
export.init();
assertThat(export.getSrcOutDir().exists());
assertThat(export.getResOutDir().exists());
export.generateGradleFiles();
}
protected String getAppGradleBuild() {
@@ -12,7 +12,7 @@ public class OptionalTargetSdkVersion extends ExportGradleTest {
void test() {
exportGradle("OptionalTargetSdkVersion.xml", "strings.xml");
assertThat(getAppGradleBuild()).contains("targetSdkVersion 14");
assertThat(getAppGradleBuild()).contains("targetSdkVersion 14").doesNotContain(" vectorDrawables.useSupportLibrary = true");
}
}
@@ -0,0 +1,28 @@
package jadx.tests.export;
import org.junit.jupiter.api.Test;
import jadx.core.export.GradleInfoStorage;
import jadx.tests.api.ExportGradleTest;
import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat;
public class VectorDrawablesUseSupportLibrary extends ExportGradleTest {
@Test
void test() {
GradleInfoStorage gradleInfo = getRootNode().getGradleInfoStorage();
gradleInfo.setVectorFillType(true);
exportGradle("OptionalTargetSdkVersion.xml", "strings.xml");
assertThat(getAppGradleBuild()).contains(" vectorDrawables.useSupportLibrary = true");
gradleInfo.setVectorFillType(false);
gradleInfo.setVectorPathData(true);
exportGradle("OptionalTargetSdkVersion.xml", "strings.xml");
assertThat(getAppGradleBuild()).contains(" vectorDrawables.useSupportLibrary = true");
exportGradle("MinSdkVersion25.xml", "strings.xml");
assertThat(getAppGradleBuild()).doesNotContain(" vectorDrawables.useSupportLibrary = true");
}
}
@@ -17,6 +17,7 @@ public class TemplateFileTest {
tmpl.add("targetSdkVersion", 2);
tmpl.add("versionCode", 3);
tmpl.add("versionName", "1.2.3");
tmpl.add("additionalOptions", "useLibrary 'org.apache.http.legacy'");
String res = tmpl.build();
System.out.println(res);
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" android:versionCode="1" android:versionName="1.0" android:compileSdkVersion="33" android:compileSdkVersionCodename="13" package="jadx.test.app" platformBuildVersionCode="33" platformBuildVersionName="13">
<uses-sdk android:minSdkVersion="25" android:targetSdkVersion="32"/>
<application android:label="JadxTestApp">
</application>
</manifest>