diff --git a/jadx-core/src/main/java/jadx/core/Jadx.java b/jadx-core/src/main/java/jadx/core/Jadx.java index 377d8c7f3e8..fea6ac920e0 100644 --- a/jadx-core/src/main/java/jadx/core/Jadx.java +++ b/jadx-core/src/main/java/jadx/core/Jadx.java @@ -54,6 +54,7 @@ import jadx.core.dex.visitors.debuginfo.DebugInfoAttachVisitor; import jadx.core.dex.visitors.finaly.MarkFinallyVisitor; import jadx.core.dex.visitors.fixaccessmodifiers.FixAccessModifiers; +import jadx.core.dex.visitors.gradle.NonFinalResIdsVisitor; import jadx.core.dex.visitors.kotlin.ProcessKotlinInternals; import jadx.core.dex.visitors.prepare.AddAndroidConstants; import jadx.core.dex.visitors.prepare.CollectConstValues; @@ -186,6 +187,7 @@ public static List getRegionsModePasses(JadxArgs args) { passes.add(new EnumVisitor()); passes.add(new FixSwitchOverEnum()); + passes.add(new NonFinalResIdsVisitor()); passes.add(new ExtractFieldInit()); passes.add(new FixAccessModifiers()); passes.add(new ClassModifier()); diff --git a/jadx-core/src/main/java/jadx/core/dex/visitors/gradle/NonFinalResIdsVisitor.java b/jadx-core/src/main/java/jadx/core/dex/visitors/gradle/NonFinalResIdsVisitor.java new file mode 100644 index 00000000000..ba0a4536f72 --- /dev/null +++ b/jadx-core/src/main/java/jadx/core/dex/visitors/gradle/NonFinalResIdsVisitor.java @@ -0,0 +1,118 @@ +package jadx.core.dex.visitors.gradle; + +import java.util.Map; + +import jadx.api.plugins.input.data.annotations.AnnotationVisibility; +import jadx.api.plugins.input.data.annotations.EncodedValue; +import jadx.api.plugins.input.data.annotations.IAnnotation; +import jadx.api.plugins.input.data.attributes.JadxAttrType; +import jadx.api.plugins.input.data.attributes.types.AnnotationsAttr; +import jadx.core.dex.attributes.nodes.CodeFeaturesAttr; +import jadx.core.dex.info.ClassInfo; +import jadx.core.dex.nodes.ClassNode; +import jadx.core.dex.nodes.FieldNode; +import jadx.core.dex.nodes.IFieldInfoRef; +import jadx.core.dex.nodes.IRegion; +import jadx.core.dex.nodes.MethodNode; +import jadx.core.dex.nodes.RootNode; +import jadx.core.dex.regions.SwitchRegion; +import jadx.core.dex.visitors.AbstractVisitor; +import jadx.core.dex.visitors.FixSwitchOverEnum; +import jadx.core.dex.visitors.JadxVisitor; +import jadx.core.dex.visitors.regions.DepthRegionTraversal; +import jadx.core.dex.visitors.regions.IRegionIterativeVisitor; +import jadx.core.export.GradleInfoStorage; +import jadx.core.utils.android.AndroidResourcesUtils; +import jadx.core.utils.exceptions.JadxException; + +@JadxVisitor( + name = "NonFinalResIdsVisitor", + desc = "Detect usage of android resource constants in cases where constant expressions are required.", + runAfter = FixSwitchOverEnum.class +) +public class NonFinalResIdsVisitor extends AbstractVisitor implements IRegionIterativeVisitor { + + private boolean nonFinalResIdsFlagRequired = false; + + private GradleInfoStorage gradleInfoStorage; + + public void init(RootNode root) throws JadxException { + gradleInfoStorage = root.getGradleInfoStorage(); + } + + @Override + public boolean visit(ClassNode cls) throws JadxException { + if (nonFinalResIdsFlagRequired) { + return false; + } + AnnotationsAttr annotationsList = cls.get(JadxAttrType.ANNOTATION_LIST); + if (visitAnnotationList(annotationsList)) { + return false; + } + return super.visit(cls); + } + + private static boolean isCustomResourceClass(ClassInfo cls) { + ClassInfo parentClass = cls.getParentClass(); + return parentClass != null && parentClass.getShortName().equals("R") && !parentClass.getFullName().equals("android.R"); + } + + @Override + public void visit(MethodNode mth) throws JadxException { + AnnotationsAttr annotationsList = mth.get(JadxAttrType.ANNOTATION_LIST); + if (visitAnnotationList(annotationsList)) { + nonFinalResIdsFlagRequired = true; + return; + } + + if (nonFinalResIdsFlagRequired || !CodeFeaturesAttr.contains(mth, CodeFeaturesAttr.CodeFeature.SWITCH)) { + return; + } + DepthRegionTraversal.traverseIterative(mth, this); + } + + private boolean visitAnnotationList(AnnotationsAttr annotationsList) { + if (annotationsList != null) { + for (IAnnotation annotation : annotationsList.getAll()) { + if (annotation.getVisibility() == AnnotationVisibility.SYSTEM) { + continue; + } + for (Map.Entry entry : annotation.getValues().entrySet()) { + Object value = entry.getValue().getValue(); + if (value instanceof IFieldInfoRef && isCustomResourceClass(((IFieldInfoRef) value).getFieldInfo().getDeclClass())) { + gradleInfoStorage.setNonFinalResIds(true); + return true; + } + } + } + } + return false; + } + + @Override + public boolean visitRegion(MethodNode mth, IRegion region) { + if (nonFinalResIdsFlagRequired) { + return false; + } + if (region instanceof SwitchRegion) { + return detectSwitchOverResIds((SwitchRegion) region); + } + return false; + } + + private boolean detectSwitchOverResIds(SwitchRegion switchRegion) { + for (SwitchRegion.CaseInfo caseInfo : switchRegion.getCases()) { + for (Object key : caseInfo.getKeys()) { + if (key instanceof FieldNode) { + ClassNode topParentClass = ((FieldNode) key).getTopParentClass(); + if (AndroidResourcesUtils.isResourceClass(topParentClass) && !"android.R".equals(topParentClass.getFullName())) { + this.nonFinalResIdsFlagRequired = true; + gradleInfoStorage.setNonFinalResIds(true); + return false; + } + } + } + } + return false; + } +} 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 d5e247352e3..62b1e4aea6c 100644 --- a/jadx-core/src/main/java/jadx/core/export/ExportGradleProject.java +++ b/jadx-core/src/main/java/jadx/core/export/ExportGradleProject.java @@ -1,7 +1,9 @@ package jadx.core.export; import java.io.File; +import java.io.FileOutputStream; import java.io.IOException; +import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.EnumSet; import java.util.List; @@ -35,11 +37,26 @@ public void generateGradleFiles() { saveProjectBuildGradle(); saveApplicationBuildGradle(); saveSettingsGradle(); + saveGradleProperties(); } catch (Exception e) { throw new JadxRuntimeException("Gradle export failed", e); } } + private void saveGradleProperties() throws IOException { + GradleInfoStorage gradleInfo = root.getGradleInfoStorage(); + /* + * For Android Gradle Plugin >=8.0.0 the property "android.nonFinalResIds=false" has to be set in + * "gradle.properties" when resource identifiers are used as constant expressions. + */ + if (gradleInfo.isNonFinalResIds()) { + File gradlePropertiesFile = new File(projectDir, "gradle.properties"); + try (FileOutputStream fos = new FileOutputStream(gradlePropertiesFile)) { + fos.write("android.nonFinalResIds=false".getBytes(StandardCharsets.UTF_8)); + } + } + } + private void saveProjectBuildGradle() throws IOException { TemplateFile tmpl = TemplateFile.fromResources("/export/build.gradle.tmpl"); tmpl.save(new File(projectDir, "build.gradle")); diff --git a/jadx-core/src/main/java/jadx/core/export/GradleInfoStorage.java b/jadx-core/src/main/java/jadx/core/export/GradleInfoStorage.java index 7f9c9b395c5..707a468d63e 100644 --- a/jadx-core/src/main/java/jadx/core/export/GradleInfoStorage.java +++ b/jadx-core/src/main/java/jadx/core/export/GradleInfoStorage.java @@ -8,6 +8,8 @@ public class GradleInfoStorage { private boolean useApacheHttpLegacy; + private boolean nonFinalResIds; + public boolean isVectorPathData() { return vectorPathData; } @@ -31,4 +33,12 @@ public boolean isUseApacheHttpLegacy() { public void setUseApacheHttpLegacy(boolean useApacheHttpLegacy) { this.useApacheHttpLegacy = useApacheHttpLegacy; } + + public boolean isNonFinalResIds() { + return nonFinalResIds; + } + + public void setNonFinalResIds(boolean nonFinalResIds) { + this.nonFinalResIds = nonFinalResIds; + } } 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 50395174ac9..804f89b7f94 100644 --- a/jadx-core/src/test/java/jadx/tests/api/ExportGradleTest.java +++ b/jadx-core/src/test/java/jadx/tests/api/ExportGradleTest.java @@ -67,4 +67,12 @@ protected String getAppGradleBuild() { protected String getSettingsGradle() { return loadFileContent(new File(exportDir, "settings.gradle")); } + + protected File getGradleProperiesFile() { + return new File(exportDir, "gradle.properties"); + } + + protected String getGradleProperies() { + return loadFileContent(getGradleProperiesFile()); + } } diff --git a/jadx-core/src/test/java/jadx/tests/export/TestNonFinalResIds.java b/jadx-core/src/test/java/jadx/tests/export/TestNonFinalResIds.java new file mode 100644 index 00000000000..e4727ab4609 --- /dev/null +++ b/jadx-core/src/test/java/jadx/tests/export/TestNonFinalResIds.java @@ -0,0 +1,24 @@ +package jadx.tests.export; + +import org.junit.jupiter.api.Assertions; +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 TestNonFinalResIds extends ExportGradleTest { + + @Test + void test() { + GradleInfoStorage gradleInfo = getRootNode().getGradleInfoStorage(); + gradleInfo.setNonFinalResIds(false); + exportGradle("OptionalTargetSdkVersion.xml", "strings.xml"); + Assertions.assertFalse(getGradleProperiesFile().exists()); + + gradleInfo.setNonFinalResIds(true); + exportGradle("OptionalTargetSdkVersion.xml", "strings.xml"); + assertThat(getGradleProperies()).containsOne("android.nonFinalResIds=false"); + } +}