Skip to content

Commit

Permalink
feat(gradle): handle non-final res ids for AGP >8.0.0 (PR #2362)
Browse files Browse the repository at this point in the history
  • Loading branch information
nitram84 authored Dec 11, 2024
1 parent 47f2e51 commit 7eab3c8
Show file tree
Hide file tree
Showing 6 changed files with 179 additions and 0 deletions.
2 changes: 2 additions & 0 deletions jadx-core/src/main/java/jadx/core/Jadx.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -186,6 +187,7 @@ public static List<IDexTreeVisitor> 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());
Expand Down
Original file line number Diff line number Diff line change
@@ -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<String, EncodedValue> 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;
}
}
17 changes: 17 additions & 0 deletions jadx-core/src/main/java/jadx/core/export/ExportGradleProject.java
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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"));
Expand Down
10 changes: 10 additions & 0 deletions jadx-core/src/main/java/jadx/core/export/GradleInfoStorage.java
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ public class GradleInfoStorage {

private boolean useApacheHttpLegacy;

private boolean nonFinalResIds;

public boolean isVectorPathData() {
return vectorPathData;
}
Expand All @@ -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;
}
}
8 changes: 8 additions & 0 deletions jadx-core/src/test/java/jadx/tests/api/ExportGradleTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -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());
}
}
24 changes: 24 additions & 0 deletions jadx-core/src/test/java/jadx/tests/export/TestNonFinalResIds.java
Original file line number Diff line number Diff line change
@@ -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");
}
}

0 comments on commit 7eab3c8

Please sign in to comment.