feat(gradle): handle non-final res ids for AGP >8.0.0 (PR #2362)

This commit is contained in:
nitram84 2024-12-11 18:56:28 +01:00 committed by GitHub
parent 47f2e516e5
commit 7eab3c8534
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 179 additions and 0 deletions

View File

@ -54,6 +54,7 @@ import jadx.core.dex.visitors.debuginfo.DebugInfoApplyVisitor;
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 class Jadx {
passes.add(new EnumVisitor());
passes.add(new FixSwitchOverEnum());
passes.add(new NonFinalResIdsVisitor());
passes.add(new ExtractFieldInit());
passes.add(new FixAccessModifiers());
passes.add(new ClassModifier());

View File

@ -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;
}
}

View File

@ -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 class ExportGradleProject {
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"));

View File

@ -8,6 +8,8 @@ public class GradleInfoStorage {
private boolean useApacheHttpLegacy;
private boolean nonFinalResIds;
public boolean isVectorPathData() {
return vectorPathData;
}
@ -31,4 +33,12 @@ public class GradleInfoStorage {
public void setUseApacheHttpLegacy(boolean useApacheHttpLegacy) {
this.useApacheHttpLegacy = useApacheHttpLegacy;
}
public boolean isNonFinalResIds() {
return nonFinalResIds;
}
public void setNonFinalResIds(boolean nonFinalResIds) {
this.nonFinalResIds = nonFinalResIds;
}
}

View File

@ -67,4 +67,12 @@ public abstract class ExportGradleTest {
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());
}
}

View File

@ -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");
}
}