Skip to content

Commit

Permalink
refactor: reuse the same parser in main activity action and gradle ex…
Browse files Browse the repository at this point in the history
…port feature (PR skylot#1971)

* internal: reuse the same parser in Main Activity action and export gradle project

* removed unnecessary logs

* fixed code formatting issues

* removed no longer used methods

* optimize imports

* fix exception when app name isn't found

* use EnumSet instead of int for parse flags

* moved ApplicationParams class under android utils package

* moved attributes parsing to a seperate method

* fallback to any strings.xml if default one is not found
  • Loading branch information
Mino260806 authored Jul 31, 2023
1 parent 2c2bb64 commit 63fc7e0
Show file tree
Hide file tree
Showing 7 changed files with 298 additions and 224 deletions.
84 changes: 13 additions & 71 deletions jadx-core/src/main/java/jadx/core/export/ExportGradleProject.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,23 +2,18 @@

import java.io.File;
import java.io.IOException;
import java.io.StringReader;
import java.util.ArrayList;
import java.util.EnumSet;
import java.util.List;
import java.util.regex.Pattern;

import javax.xml.parsers.DocumentBuilder;

import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;
import org.xml.sax.InputSource;

import jadx.api.ResourceFile;
import jadx.core.dex.nodes.RootNode;
import jadx.core.utils.android.AndroidManifestParser;
import jadx.core.utils.android.AppAttribute;
import jadx.core.utils.android.ApplicationParams;
import jadx.core.utils.exceptions.JadxRuntimeException;
import jadx.core.xmlgen.ResContainer;
import jadx.core.xmlgen.XmlSecurity;

public class ExportGradleProject {
private static final Pattern ILLEGAL_GRADLE_CHARS = Pattern.compile("[/\\\\:>\"?*|]");
Expand All @@ -32,9 +27,7 @@ public ExportGradleProject(RootNode root, File projectDir, ResourceFile androidM
this.root = root;
this.projectDir = projectDir;
this.appDir = new File(projectDir, "app");
this.applicationParams = getApplicationParams(
parseAndroidManifest(androidManifest),
parseAppStrings(appStrings));
this.applicationParams = getApplicationParams(androidManifest, appStrings);
}

public void generateGradleFiles() {
Expand Down Expand Up @@ -96,64 +89,13 @@ private void genAdditionalAndroidPluginOptions(TemplateFile tmpl, List<String> a
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);
Element application = (Element) androidManifest.getElementsByTagName("application").item(0);

Integer versionCode = Integer.valueOf(manifest.getAttribute("android:versionCode"));
String versionName = manifest.getAttribute("android:versionName");
Integer minSdk = Integer.valueOf(usesSdk.getAttribute("android:minSdkVersion"));
String stringTargetSdk = usesSdk.getAttribute("android:targetSdkVersion");
Integer targetSdk = stringTargetSdk.isEmpty() ? minSdk : Integer.valueOf(stringTargetSdk);
String appName = "UNKNOWN";

if (application.hasAttribute("android:label")) {
String appLabelName = application.getAttribute("android:label");
if (appLabelName.startsWith("@string")) {
appLabelName = appLabelName.split("/")[1];
NodeList strings = appStrings.getElementsByTagName("string");

for (int i = 0; i < strings.getLength(); i++) {
String stringName = strings.item(i)
.getAttributes()
.getNamedItem("name")
.getNodeValue();

if (stringName.equals(appLabelName)) {
appName = strings.item(i).getTextContent();
break;
}
}
} else {
appName = appLabelName;
}
}
return new ApplicationParams(appName, minSdk, targetSdk, versionCode, versionName);
}

private Document parseXml(String xmlContent) {
try {
DocumentBuilder builder = XmlSecurity.getSecureDbf().newDocumentBuilder();
Document document = builder.parse(new InputSource(new StringReader(xmlContent)));

document.getDocumentElement().normalize();

return document;
} catch (Exception e) {
throw new JadxRuntimeException("Can not parse xml content", e);
}
}

private Document parseAppStrings(ResContainer appStrings) {
String content = appStrings.getText().getCodeStr();

return parseXml(content);
}

private Document parseAndroidManifest(ResourceFile androidManifest) {
String content = androidManifest.loadContent().getText().getCodeStr();

return parseXml(content);
private ApplicationParams getApplicationParams(ResourceFile androidManifest, ResContainer appStrings) {
AndroidManifestParser parser = new AndroidManifestParser(androidManifest, appStrings, EnumSet.of(
AppAttribute.APPLICATION_LABEL,
AppAttribute.MIN_SDK_VERSION,
AppAttribute.TARGET_SDK_VERSION,
AppAttribute.VERSION_CODE,
AppAttribute.VERSION_NAME));
return parser.parse();
}
}
22 changes: 14 additions & 8 deletions jadx-core/src/main/java/jadx/core/export/ExportGradleTask.java
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import jadx.api.ResourceType;
import jadx.api.TaskBarrier;
import jadx.core.dex.nodes.RootNode;
import jadx.core.utils.android.AndroidManifestParser;
import jadx.core.utils.exceptions.JadxRuntimeException;
import jadx.core.utils.files.FileUtils;
import jadx.core.xmlgen.ResContainer;
Expand Down Expand Up @@ -35,21 +36,26 @@ public ExportGradleTask(List<ResourceFile> resources, RootNode root, File projec

@Override
public void run() {
ResourceFile androidManifest = resources.stream()
.filter(resourceFile -> resourceFile.getType() == ResourceType.MANIFEST)
.findFirst()
.orElseThrow(IllegalStateException::new);
ResourceFile androidManifest = AndroidManifestParser.getAndroidManifest(resources);
if (androidManifest == null) {
throw new IllegalStateException("Could not find AndroidManifest.xml");
}

ResContainer strings = resources.stream()
List<ResContainer> resContainers = resources.stream()
.filter(resourceFile -> resourceFile.getType() == ResourceType.ARSC)
.findFirst()
.orElseThrow(IllegalStateException::new)
.loadContent()
.getSubFiles()
.getSubFiles();

ResContainer strings = resContainers
.stream()
.filter(resContainer -> resContainer.getFileName().contains("strings.xml"))
.filter(resContainer -> resContainer.getName().contains("values/strings.xml"))
.findFirst()
.orElseThrow(IllegalStateException::new);
.orElseGet(() -> resContainers.stream()
.filter(resContainer -> resContainer.getFileName().contains("strings.xml"))
.findFirst()
.orElseThrow(IllegalStateException::new));

ExportGradleProject export = new ExportGradleProject(root, projectDir, androidManifest, strings);

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,230 @@
package jadx.core.utils.android;

import java.io.StringReader;
import java.util.EnumSet;
import java.util.List;

import javax.xml.parsers.DocumentBuilder;

import org.jetbrains.annotations.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;
import org.xml.sax.InputSource;

import jadx.api.ResourceFile;
import jadx.api.ResourceType;
import jadx.core.utils.exceptions.JadxRuntimeException;
import jadx.core.xmlgen.ResContainer;
import jadx.core.xmlgen.XmlSecurity;

public class AndroidManifestParser {
private static final Logger LOG = LoggerFactory.getLogger(AndroidManifestParser.class);

private final Document androidManifest;
private final Document appStrings;
private final EnumSet<AppAttribute> parseAttrs;

public AndroidManifestParser(ResourceFile androidManifestRes, EnumSet<AppAttribute> parseAttrs) {
this(androidManifestRes, null, parseAttrs);
}

public AndroidManifestParser(ResourceFile androidManifestRes, ResContainer appStrings, EnumSet<AppAttribute> parseAttrs) {
this.parseAttrs = parseAttrs;

this.androidManifest = parseAndroidManifest(androidManifestRes);
this.appStrings = parseAppStrings(appStrings);

validateAttrs();
}

public boolean isManifestFound() {
return androidManifest != null;
}

@Nullable
public static ResourceFile getAndroidManifest(List<ResourceFile> resources) {
return resources.stream()
.filter(resourceFile -> resourceFile.getType() == ResourceType.MANIFEST)
.findFirst()
.orElse(null);
}

public ApplicationParams parse() {
if (!isManifestFound()) {
throw new JadxRuntimeException("AndroidManifest.xml is missing");
}

return parseAttributes();
}

private void validateAttrs() {
if (parseAttrs.contains(AppAttribute.APPLICATION_LABEL) && appStrings == null) {
throw new IllegalArgumentException("APPLICATION_LABEL attribute requires non null appStrings");
}
}

private ApplicationParams parseAttributes() {
String applicationLabel = null;
Integer minSdkVersion = null;
Integer targetSdkVersion = null;
Integer versionCode = null;
String versionName = null;
String mainActivity = null;

Element manifest = (Element) androidManifest.getElementsByTagName("manifest").item(0);
Element usesSdk = (Element) androidManifest.getElementsByTagName("uses-sdk").item(0);

if (parseAttrs.contains(AppAttribute.APPLICATION_LABEL)) {
applicationLabel = getApplicationLabel();
}
if (parseAttrs.contains(AppAttribute.MIN_SDK_VERSION)) {
minSdkVersion = Integer.valueOf(usesSdk.getAttribute("android:minSdkVersion"));
}
if (parseAttrs.contains(AppAttribute.TARGET_SDK_VERSION)) {
String stringTargetSdk = usesSdk.getAttribute("android:targetSdkVersion");
if (!stringTargetSdk.isEmpty()) {
targetSdkVersion = Integer.valueOf(stringTargetSdk);
} else {
if (minSdkVersion == null) {
minSdkVersion = Integer.valueOf(usesSdk.getAttribute("android:minSdkVersion"));
}
targetSdkVersion = minSdkVersion;
}
}
if (parseAttrs.contains(AppAttribute.VERSION_CODE)) {
versionCode = Integer.valueOf(manifest.getAttribute("android:versionCode"));
}
if (parseAttrs.contains(AppAttribute.VERSION_NAME)) {
versionName = manifest.getAttribute("android:versionName");
}
if (parseAttrs.contains(AppAttribute.MAIN_ACTIVITY)) {
mainActivity = getMainActivityName();
}

return new ApplicationParams(applicationLabel, minSdkVersion, targetSdkVersion, versionCode,
versionName, mainActivity);
}

private String getApplicationLabel() {
Element application = (Element) androidManifest.getElementsByTagName("application").item(0);
if (application.hasAttribute("android:label")) {
String appLabelName = application.getAttribute("android:label");
if (appLabelName.startsWith("@string")) {
appLabelName = appLabelName.split("/")[1];
NodeList strings = appStrings.getElementsByTagName("string");

for (int i = 0; i < strings.getLength(); i++) {
String stringName = strings.item(i)
.getAttributes()
.getNamedItem("name")
.getNodeValue();

if (stringName.equals(appLabelName)) {
return strings.item(i).getTextContent();
}
}
} else {
return appLabelName;
}
}

return "UNKNOWN";
}

private String getMainActivityName() {
String mainActivityName = getMainActivityNameThroughActivityTag();
if (mainActivityName == null) {
mainActivityName = getMainActivityNameThroughActivityAliasTag();
}
return mainActivityName;
}

private String getMainActivityNameThroughActivityAliasTag() {
NodeList activityAliasNodes = androidManifest.getElementsByTagName("activity-alias");
for (int i = 0; i < activityAliasNodes.getLength(); i++) {
Element activityElement = (Element) activityAliasNodes.item(i);
if (isMainActivityElement(activityElement)) {
return activityElement.getAttribute("android:targetActivity");
}
}
return null;
}

private String getMainActivityNameThroughActivityTag() {
NodeList activityNodes = androidManifest.getElementsByTagName("activity");
for (int i = 0; i < activityNodes.getLength(); i++) {
Element activityElement = (Element) activityNodes.item(i);
if (isMainActivityElement(activityElement)) {
return activityElement.getAttribute("android:name");
}
}
return null;
}

private boolean isMainActivityElement(Element element) {
NodeList intentFilterNodes = element.getElementsByTagName("intent-filter");
for (int j = 0; j < intentFilterNodes.getLength(); j++) {
Element intentFilterElement = (Element) intentFilterNodes.item(j);
NodeList actionNodes = intentFilterElement.getElementsByTagName("action");
NodeList categoryNodes = intentFilterElement.getElementsByTagName("category");

boolean isMainAction = false;
boolean isLauncherCategory = false;

for (int k = 0; k < actionNodes.getLength(); k++) {
Element actionElement = (Element) actionNodes.item(k);
String actionName = actionElement.getAttribute("android:name");
if ("android.intent.action.MAIN".equals(actionName)) {
isMainAction = true;
break;
}
}

for (int k = 0; k < categoryNodes.getLength(); k++) {
Element categoryElement = (Element) categoryNodes.item(k);
String categoryName = categoryElement.getAttribute("android:name");
if ("android.intent.category.LAUNCHER".equals(categoryName)) {
isLauncherCategory = true;
break;
}
}

if (isMainAction && isLauncherCategory) {
return true;
}
}
return false;
}

private static Document parseXml(String xmlContent) {
try {
DocumentBuilder builder = XmlSecurity.getSecureDbf().newDocumentBuilder();
Document document = builder.parse(new InputSource(new StringReader(xmlContent)));

document.getDocumentElement().normalize();

return document;
} catch (Exception e) {
throw new JadxRuntimeException("Can not parse xml content", e);
}
}

private static Document parseAppStrings(ResContainer appStrings) {
if (appStrings == null) {
return null;
}

String content = appStrings.getText().getCodeStr();

return parseXml(content);
}

private static Document parseAndroidManifest(ResourceFile androidManifest) {
String content = androidManifest.loadContent().getText().getCodeStr();

return parseXml(content);
}
}
10 changes: 10 additions & 0 deletions jadx-core/src/main/java/jadx/core/utils/android/AppAttribute.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package jadx.core.utils.android;

public enum AppAttribute {
APPLICATION_LABEL,
MIN_SDK_VERSION,
TARGET_SDK_VERSION,
VERSION_CODE,
VERSION_NAME,
MAIN_ACTIVITY,
}
Loading

0 comments on commit 63fc7e0

Please sign in to comment.