diff --git a/jadx-core/src/main/java/jadx/api/JadxDecompiler.java b/jadx-core/src/main/java/jadx/api/JadxDecompiler.java index 7f64165e6..cbea776a5 100644 --- a/jadx-core/src/main/java/jadx/api/JadxDecompiler.java +++ b/jadx-core/src/main/java/jadx/api/JadxDecompiler.java @@ -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 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 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 tasks, File outDir) { + private int appendResourcesSaveTasks(List 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 tasks, File outDir) { + private int appendSourcesSave(List tasks, File outDir, TaskBarrier barrier) { + int numSourceTasks = 0; Predicate classFilter = args.getClassFilter(); List classes = getClasses(); List processQueue = new ArrayList<>(classes.size()); @@ -369,17 +363,25 @@ public final class JadxDecompiler implements Closeable { } for (List 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 getClasses() { diff --git a/jadx-core/src/main/java/jadx/api/TaskBarrier.java b/jadx-core/src/main/java/jadx/api/TaskBarrier.java new file mode 100644 index 000000000..3db8dc8a2 --- /dev/null +++ b/jadx-core/src/main/java/jadx/api/TaskBarrier.java @@ -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(); + } + } +} diff --git a/jadx-core/src/main/java/jadx/core/dex/nodes/RootNode.java b/jadx-core/src/main/java/jadx/core/dex/nodes/RootNode.java index c32600d31..05b0cd1a9 100644 --- a/jadx-core/src/main/java/jadx/core/dex/nodes/RootNode.java +++ b/jadx-core/src/main/java/jadx/core/dex/nodes/RootNode.java @@ -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 clsMap = new HashMap<>(); private final Map rawClsMap = new HashMap<>(); private List classes = new ArrayList<>(); @@ -712,4 +715,8 @@ public class RootNode { public boolean isProto() { return isProto; } + + public GradleInfoStorage getGradleInfoStorage() { + return gradleInfoStorage; + } } diff --git a/jadx-core/src/main/java/jadx/core/export/ExportGradleProject.java b/jadx-core/src/main/java/jadx/core/export/ExportGradleProject.java index 69ba673ab..bc8cf3afe 100644 --- a/jadx-core/src/main/java/jadx/core/export/ExportGradleProject.java +++ b/jadx-core/src/main/java/jadx/core/export/ExportGradleProject.java @@ -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 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 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; - } } diff --git a/jadx-core/src/main/java/jadx/core/export/ExportGradleTask.java b/jadx-core/src/main/java/jadx/core/export/ExportGradleTask.java new file mode 100644 index 000000000..1ff62e753 --- /dev/null +++ b/jadx-core/src/main/java/jadx/core/export/ExportGradleTask.java @@ -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 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 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; + } +} diff --git a/jadx-core/src/main/java/jadx/core/export/GradleInfoStorage.java b/jadx-core/src/main/java/jadx/core/export/GradleInfoStorage.java new file mode 100644 index 000000000..0ac94e1d2 --- /dev/null +++ b/jadx-core/src/main/java/jadx/core/export/GradleInfoStorage.java @@ -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; + } +} diff --git a/jadx-core/src/main/java/jadx/core/xmlgen/BinaryXMLParser.java b/jadx-core/src/main/java/jadx/core/xmlgen/BinaryXMLParser.java index c3c3c169e..1031994f9 100644 --- a/jadx-core/src/main/java/jadx/core/xmlgen/BinaryXMLParser.java +++ b/jadx-core/src/main/java/jadx/core/xmlgen/BinaryXMLParser.java @@ -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('"'); } diff --git a/jadx-core/src/main/java/jadx/core/xmlgen/ResourcesSaver.java b/jadx-core/src/main/java/jadx/core/xmlgen/ResourcesSaver.java index 5e0d3b2e1..9845c2cf6 100644 --- a/jadx-core/src/main/java/jadx/core/xmlgen/ResourcesSaver.java +++ b/jadx-core/src/main/java/jadx/core/xmlgen/ResourcesSaver.java @@ -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(); + } } } diff --git a/jadx-core/src/main/resources/export/app.build.gradle.tmpl b/jadx-core/src/main/resources/export/app.build.gradle.tmpl index bd6e36813..10f439517 100644 --- a/jadx-core/src/main/resources/export/app.build.gradle.tmpl +++ b/jadx-core/src/main/resources/export/app.build.gradle.tmpl @@ -12,7 +12,7 @@ android { targetSdkVersion {{targetSdkVersion}} versionCode {{versionCode}} versionName "{{versionName}}" - +{{additionalOptions}} testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } diff --git a/jadx-core/src/test/java/jadx/tests/api/ExportGradleTest.java b/jadx-core/src/test/java/jadx/tests/api/ExportGradleTest.java index 6fa37e161..b84fc5d6d 100644 --- a/jadx-core/src/test/java/jadx/tests/api/ExportGradleTest.java +++ b/jadx-core/src/test/java/jadx/tests/api/ExportGradleTest.java @@ -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() { diff --git a/jadx-core/src/test/java/jadx/tests/export/OptionalTargetSdkVersion.java b/jadx-core/src/test/java/jadx/tests/export/OptionalTargetSdkVersion.java index f4561140f..137bf026f 100644 --- a/jadx-core/src/test/java/jadx/tests/export/OptionalTargetSdkVersion.java +++ b/jadx-core/src/test/java/jadx/tests/export/OptionalTargetSdkVersion.java @@ -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"); } } diff --git a/jadx-core/src/test/java/jadx/tests/export/VectorDrawablesUseSupportLibrary.java b/jadx-core/src/test/java/jadx/tests/export/VectorDrawablesUseSupportLibrary.java new file mode 100644 index 000000000..1ae659b6b --- /dev/null +++ b/jadx-core/src/test/java/jadx/tests/export/VectorDrawablesUseSupportLibrary.java @@ -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"); + + } +} diff --git a/jadx-core/src/test/java/jadx/tests/functional/TemplateFileTest.java b/jadx-core/src/test/java/jadx/tests/functional/TemplateFileTest.java index 81d615428..753ad1f55 100644 --- a/jadx-core/src/test/java/jadx/tests/functional/TemplateFileTest.java +++ b/jadx-core/src/test/java/jadx/tests/functional/TemplateFileTest.java @@ -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); diff --git a/jadx-core/src/test/manifest/MinSdkVersion25.xml b/jadx-core/src/test/manifest/MinSdkVersion25.xml new file mode 100644 index 000000000..1c23402b9 --- /dev/null +++ b/jadx-core/src/test/manifest/MinSdkVersion25.xml @@ -0,0 +1,6 @@ + + + + + +