refactor: reuse the same parser in main activity action and gradle export feature (PR #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
This commit is contained in:
Mino
2023-07-31 18:50:47 +01:00
committed by GitHub
parent 2c2bb64c09
commit 63fc7e05b6
7 changed files with 298 additions and 224 deletions
@@ -2,23 +2,18 @@ package jadx.core.export;
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("[/\\\\:>\"?*|]");
@@ -32,9 +27,7 @@ public class ExportGradleProject {
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() {
@@ -96,64 +89,13 @@ public class ExportGradleProject {
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();
}
}
@@ -7,6 +7,7 @@ import jadx.api.ResourceFile;
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;
@@ -35,21 +36,26 @@ public class ExportGradleTask implements Runnable {
@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);
@@ -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);
}
}
@@ -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,
}
@@ -1,4 +1,7 @@
package jadx.core.export;
package jadx.core.utils.android;
import jadx.api.JadxDecompiler;
import jadx.api.JavaClass;
public class ApplicationParams {
@@ -7,14 +10,16 @@ public class ApplicationParams {
private final Integer targetSdkVersion;
private final Integer versionCode;
private final String versionName;
private final String mainActivtiy;
public ApplicationParams(String applicationLabel, Integer minSdkVersion, Integer targetSdkVersion, Integer versionCode,
String versionName) {
String versionName, String mainActivtiy) {
this.applicationLabel = applicationLabel;
this.minSdkVersion = minSdkVersion;
this.targetSdkVersion = targetSdkVersion;
this.versionCode = versionCode;
this.versionName = versionName;
this.mainActivtiy = mainActivtiy;
}
public String getApplicationName() {
@@ -36,4 +41,12 @@ public class ApplicationParams {
public String getVersionName() {
return versionName;
}
public String getMainActivityName() {
return mainActivtiy;
}
public JavaClass getMainActivity(JadxDecompiler decompiler) {
return decompiler.searchJavaClassByOrigFullName(mainActivtiy);
}
}
@@ -32,6 +32,7 @@ import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.EnumSet;
import java.util.List;
import java.util.Locale;
import java.util.Timer;
@@ -89,6 +90,9 @@ import jadx.core.export.TemplateFile;
import jadx.core.plugins.events.JadxEventsImpl;
import jadx.core.utils.ListUtils;
import jadx.core.utils.StringUtils;
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.utils.files.FileUtils;
import jadx.gui.JadxWrapper;
@@ -134,7 +138,6 @@ import jadx.gui.ui.treenodes.SummaryNode;
import jadx.gui.update.JadxUpdate;
import jadx.gui.update.JadxUpdate.IUpdateCallback;
import jadx.gui.update.data.Release;
import jadx.gui.utils.AndroidManifestParser;
import jadx.gui.utils.CacheObject;
import jadx.gui.utils.FontUtils;
import jadx.gui.utils.ILoadListener;
@@ -1033,8 +1036,10 @@ public class MainWindow extends JFrame {
Action gotoMainActivityAction = new AbstractAction(NLS.str("menu.goto_main_activity"), ICON_MAIN_ACTIVITY) {
@Override
public void actionPerformed(ActionEvent ev) {
AndroidManifestParser parser = new AndroidManifestParser(getWrapper().getResources());
if (!parser.isResourceFound()) {
AndroidManifestParser parser = new AndroidManifestParser(
AndroidManifestParser.getAndroidManifest(getWrapper().getResources()),
EnumSet.of(AppAttribute.MAIN_ACTIVITY));
if (!parser.isManifestFound()) {
JOptionPane.showMessageDialog(MainWindow.this,
NLS.str("error_dialog.not_found", "AndroidManifest.xml"),
NLS.str("error_dialog.title"),
@@ -1042,7 +1047,14 @@ public class MainWindow extends JFrame {
return;
}
try {
JavaClass mainActivityClass = parser.getMainActivity(getWrapper());
ApplicationParams results = parser.parse();
if (results.getMainActivityName() == null) {
throw new JadxRuntimeException("Failed to get main activity name from manifest");
}
JavaClass mainActivityClass = results.getMainActivity(getWrapper().getDecompiler());
if (mainActivityClass == null) {
throw new JadxRuntimeException("Failed to find main activity class: " + results.getMainActivityName());
}
tabbedPane.codeJump(getCacheObject().getNodeCache().makeFrom(mainActivityClass));
} catch (Exception e) {
LOG.error("Main activity not found", e);
@@ -1,139 +0,0 @@
package jadx.gui.utils;
import java.io.IOException;
import java.io.StringReader;
import java.util.List;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.ParserConfigurationException;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import io.reactivex.annotations.Nullable;
import jadx.api.JavaClass;
import jadx.api.ResourceFile;
import jadx.api.ResourceType;
import jadx.core.utils.exceptions.JadxRuntimeException;
import jadx.core.xmlgen.XmlSecurity;
import jadx.gui.JadxWrapper;
public class AndroidManifestParser {
private final Document mXmlDocument;
public AndroidManifestParser(List<ResourceFile> resourceFiles) {
mXmlDocument = parseAndroidManifest(getAndroidManifest(resourceFiles));
}
public boolean isResourceFound() {
return mXmlDocument != null;
}
public JavaClass getMainActivity(JadxWrapper decompiler) {
final String mainActivityName = getMainActivityName();
if (mainActivityName == null) {
throw new JadxRuntimeException("Failed to get main activity name from manifest");
}
JavaClass javaClass = decompiler.searchJavaClassByOrigClassName(mainActivityName);
if (javaClass == null) {
throw new JadxRuntimeException("Failed to find main activity class: " + mainActivityName);
}
return javaClass;
}
private String getMainActivityName() {
String mainActivityName = getMainActivityNameThroughActivityTag();
if (mainActivityName == null) {
mainActivityName = getMainActivityNameThroughActivityAliasTag();
}
return mainActivityName;
}
private String getMainActivityNameThroughActivityAliasTag() {
NodeList activityAliasNodes = mXmlDocument.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 = mXmlDocument.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;
}
@Nullable
public static ResourceFile getAndroidManifest(List<ResourceFile> resources) {
// TODO: taken from ExportGradleTask.java: also use this function there ?
return resources.stream()
.filter(resourceFile -> resourceFile.getType() == ResourceType.MANIFEST)
.findFirst()
.orElse(null);
}
public static Document parseAndroidManifest(ResourceFile androidManifest) {
if (androidManifest == null) {
return null;
}
// TODO: taken from ExportGradleProject.java: also use this function there ?
Document androidManifestDocument;
try {
String xmlContent = androidManifest.loadContent().getText().getCodeStr();
DocumentBuilder builder = XmlSecurity.getSecureDbf().newDocumentBuilder();
androidManifestDocument = builder.parse(new InputSource(new StringReader(xmlContent)));
androidManifestDocument.getDocumentElement().normalize();
} catch (ParserConfigurationException | SAXException | IOException ex) {
throw new RuntimeException(ex);
}
return androidManifestDocument;
}
}