Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

gradle: handle non-final res ids for AGP >8.0.0 #2362

Merged
merged 1 commit into from
Dec 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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");
}
}
Loading