diff --git a/README.md b/README.md
index cadd85d3d..958051c1b 100644
--- a/README.md
+++ b/README.md
@@ -54,31 +54,38 @@ Run **jadx** on itself:
### Usage
```
-jadx[-gui] [options] (.dex, .apk, .jar or .class)
+jadx[-gui] [options] (.apk, .dex, .jar, .class, .smali, .zip, .aar, .arsc)
options:
- -d, --output-dir - output directory
- -ds, --output-dir-src - output directory for sources
- -dr, --output-dir-res - output directory for resources
- -j, --threads-count - processing threads count
- -r, --no-res - do not decode resources
- -s, --no-src - do not decompile source code
- -e, --export-gradle - save as android gradle project
- --show-bad-code - show inconsistent code (incorrectly decompiled)
- --no-imports - disable use of imports, always write entire package name
- --no-replace-consts - don't replace constant value with matching constant field
- --escape-unicode - escape non latin characters in strings (with \u)
- --deobf - activate deobfuscation
- --deobf-min - min length of name
- --deobf-max - max length of name
- --deobf-rewrite-cfg - force to save deobfuscation map
- --deobf-use-sourcename - use source file name as class name alias
- --cfg - save methods control flow graph to dot file
- --raw-cfg - save methods control flow graph (use raw instructions)
- -f, --fallback - make simple dump (using goto instead of 'if', 'for', etc)
- -v, --verbose - verbose output
- -h, --help - print this help
+ -d, --output-dir - output directory
+ -ds, --output-dir-src - output directory for sources
+ -dr, --output-dir-res - output directory for resources
+ -j, --threads-count - processing threads count
+ -r, --no-res - do not decode resources
+ -s, --no-src - do not decompile source code
+ -e, --export-gradle - save as android gradle project
+ --show-bad-code - show inconsistent code (incorrectly decompiled)
+ --no-imports - disable use of imports, always write entire package name
+ --no-debug-info - disable debug info
+ --no-inline-anonymous - disable anonymous classes inline
+ --no-replace-consts - don't replace constant value with matching constant field
+ --escape-unicode - escape non latin characters in strings (with \u)
+ --respect-bytecode-access-modifiers - don't change original access modifiers
+ --deobf - activate deobfuscation
+ --deobf-min - min length of name, renamed if shorter (default: 3)
+ --deobf-max - max length of name, renamed if longer (default: 64)
+ --deobf-rewrite-cfg - force to save deobfuscation map
+ --deobf-use-sourcename - use source file name as class name alias
+ --rename-flags - what to rename, comma-separated, 'case' for system case sensitivity, 'valid' for java identifiers, 'printable' characters, 'none' or 'all'
+ --cfg - save methods control flow graph to dot file
+ --raw-cfg - save methods control flow graph (use raw instructions)
+ -f, --fallback - make simple dump (using goto instead of 'if', 'for', etc)
+ -v, --verbose - verbose output
+ --version - print jadx version
+ -h, --help - print this help
Example:
jadx -d out classes.dex
+ jadx --rename-flags "none" classes.dex
+ jadx --rename-flags "valid,printable" classes.dex
```
These options also worked on jadx-gui running from command line and override options from preferences dialog
diff --git a/jadx-cli/src/main/java/jadx/cli/JadxCLIArgs.java b/jadx-cli/src/main/java/jadx/cli/JadxCLIArgs.java
index 71b39ec8d..1131c5910 100644
--- a/jadx-cli/src/main/java/jadx/cli/JadxCLIArgs.java
+++ b/jadx-cli/src/main/java/jadx/cli/JadxCLIArgs.java
@@ -57,6 +57,9 @@ public class JadxCLIArgs {
@Parameter(names = {"--no-debug-info"}, description = "disable debug info")
protected boolean debugInfo = true;
+ @Parameter(names = { "--no-inline-anonymous" }, description = "disable anonymous classes inline")
+ protected boolean inlineAnonymousClasses = true;
+
@Parameter(names = "--no-replace-consts", description = "don't replace constant value with matching constant field")
protected boolean replaceConsts = true;
@@ -175,6 +178,7 @@ public class JadxCLIArgs {
args.setExportAsGradleProject(exportAsGradleProject);
args.setUseImports(useImports);
args.setDebugInfo(debugInfo);
+ args.setInlineAnonymousClasses(inlineAnonymousClasses);
args.setRenameCaseSensitive(isRenameCaseSensitive());
args.setRenameValid(isRenameValid());
args.setRenamePrintable(isRenamePrintable());
@@ -225,6 +229,10 @@ public class JadxCLIArgs {
return debugInfo;
}
+ public boolean isInlineAnonymousClasses() {
+ return inlineAnonymousClasses;
+ }
+
public boolean isDeobfuscationOn() {
return deobfuscationOn;
}
diff --git a/jadx-core/src/main/java/jadx/api/JadxArgs.java b/jadx-core/src/main/java/jadx/api/JadxArgs.java
index df4de620d..8a832c8cb 100644
--- a/jadx-core/src/main/java/jadx/api/JadxArgs.java
+++ b/jadx-core/src/main/java/jadx/api/JadxArgs.java
@@ -31,6 +31,7 @@ public class JadxArgs {
private boolean useImports = true;
private boolean debugInfo = true;
+ private boolean inlineAnonymousClasses = true;
private boolean skipResources = false;
private boolean skipSources = false;
@@ -155,6 +156,14 @@ public class JadxArgs {
this.debugInfo = debugInfo;
}
+ public boolean isInlineAnonymousClasses() {
+ return inlineAnonymousClasses;
+ }
+
+ public void setInlineAnonymousClasses(boolean inlineAnonymousClasses) {
+ this.inlineAnonymousClasses = inlineAnonymousClasses;
+ }
+
public boolean isSkipResources() {
return skipResources;
}
diff --git a/jadx-core/src/main/java/jadx/core/Jadx.java b/jadx-core/src/main/java/jadx/core/Jadx.java
index d961f1be9..91ea09058 100644
--- a/jadx-core/src/main/java/jadx/core/Jadx.java
+++ b/jadx-core/src/main/java/jadx/core/Jadx.java
@@ -26,6 +26,7 @@ import jadx.core.dex.visitors.MarkFinallyVisitor;
import jadx.core.dex.visitors.MethodInlineVisitor;
import jadx.core.dex.visitors.ModVisitor;
import jadx.core.dex.visitors.PrepareForCodeGen;
+import jadx.core.dex.visitors.ProcessAnonymous;
import jadx.core.dex.visitors.ReSugarCode;
import jadx.core.dex.visitors.RenameVisitor;
import jadx.core.dex.visitors.SimplifyVisitor;
@@ -104,6 +105,7 @@ public class Jadx {
passes.add(new ExtractFieldInit());
passes.add(new FixAccessModifiers());
+ passes.add(new ProcessAnonymous());
passes.add(new ClassModifier());
passes.add(new MethodInlineVisitor());
passes.add(new EnumVisitor());
diff --git a/jadx-core/src/main/java/jadx/core/codegen/ClassGen.java b/jadx-core/src/main/java/jadx/core/codegen/ClassGen.java
index 288f81c64..46043879d 100644
--- a/jadx-core/src/main/java/jadx/core/codegen/ClassGen.java
+++ b/jadx-core/src/main/java/jadx/core/codegen/ClassGen.java
@@ -228,8 +228,7 @@ public class ClassGen {
private void addInnerClasses(CodeWriter code, ClassNode cls) throws CodegenException {
for (ClassNode innerCls : cls.getInnerClasses()) {
- if (innerCls.contains(AFlag.DONT_GENERATE)
- || innerCls.contains(AFlag.ANONYMOUS_CLASS)) {
+ if (innerCls.contains(AFlag.DONT_GENERATE)) {
continue;
}
ClassGen inClGen = new ClassGen(innerCls, getParentGen());
diff --git a/jadx-core/src/main/java/jadx/core/codegen/InsnGen.java b/jadx-core/src/main/java/jadx/core/codegen/InsnGen.java
index 5ad108b12..a1a2df307 100644
--- a/jadx-core/src/main/java/jadx/core/codegen/InsnGen.java
+++ b/jadx-core/src/main/java/jadx/core/codegen/InsnGen.java
@@ -556,7 +556,7 @@ public class InsnGen {
private void makeConstructor(ConstructorInsn insn, CodeWriter code)
throws CodegenException {
ClassNode cls = mth.dex().resolveClass(insn.getClassType());
- if (cls != null && cls.contains(AFlag.ANONYMOUS_CLASS) && !fallback) {
+ if (cls != null && cls.isAnonymous() && !fallback) {
inlineAnonymousConstructor(code, cls, insn);
return;
}
diff --git a/jadx-core/src/main/java/jadx/core/dex/nodes/ClassNode.java b/jadx-core/src/main/java/jadx/core/dex/nodes/ClassNode.java
index 5c90d3c16..f440dbd8a 100644
--- a/jadx-core/src/main/java/jadx/core/dex/nodes/ClassNode.java
+++ b/jadx-core/src/main/java/jadx/core/dex/nodes/ClassNode.java
@@ -122,7 +122,6 @@ public class ClassNode extends LineAttrNode implements ILoadable, ICodeNode {
accFlagsValue = cls.getAccessFlags();
}
this.accessFlags = new AccessInfo(accFlagsValue, AFType.CLASS);
- markAnonymousClass();
buildCache();
} catch (Exception e) {
throw new JadxRuntimeException("Error decode class: " + clsInfo, e);
@@ -401,41 +400,8 @@ public class ClassNode extends LineAttrNode implements ILoadable, ICodeNode {
&& getSuperClass().getObject().equals(ArgType.ENUM.getObject());
}
- public boolean markAnonymousClass() {
- if (isAnonymous() || isLambdaCls()) {
- add(AFlag.ANONYMOUS_CLASS);
- add(AFlag.DONT_GENERATE);
-
- for (MethodNode mth : getMethods()) {
- if (mth.isConstructor()) {
- mth.add(AFlag.ANONYMOUS_CONSTRUCTOR);
- }
- }
- return true;
- }
- return false;
- }
-
public boolean isAnonymous() {
- return clsInfo.isInner()
- && Character.isDigit(clsInfo.getShortName().charAt(0))
- && methods.stream().filter(MethodNode::isConstructor).count() == 1;
- }
-
- public boolean isLambdaCls() {
- return accessFlags.isSynthetic() && accessFlags.isFinal()
- && clsInfo.getType().getObject().contains(".-$$Lambda$")
- && countStaticFields() == 0;
- }
-
- private int countStaticFields() {
- int c = 0;
- for (FieldNode field : fields) {
- if (field.getAccessFlags().isStatic()) {
- c++;
- }
- }
- return c;
+ return contains(AFlag.ANONYMOUS_CLASS);
}
@Nullable
diff --git a/jadx-core/src/main/java/jadx/core/dex/visitors/ClassModifier.java b/jadx-core/src/main/java/jadx/core/dex/visitors/ClassModifier.java
index 74ec8533f..6bbde5741 100644
--- a/jadx-core/src/main/java/jadx/core/dex/visitors/ClassModifier.java
+++ b/jadx-core/src/main/java/jadx/core/dex/visitors/ClassModifier.java
@@ -37,7 +37,8 @@ import jadx.core.utils.exceptions.JadxException;
desc = "Remove synthetic classes, methods and fields",
runAfter = {
ModVisitor.class,
- FixAccessModifiers.class
+ FixAccessModifiers.class,
+ ProcessAnonymous.class
}
)
public class ClassModifier extends AbstractVisitor {
@@ -51,7 +52,6 @@ public class ClassModifier extends AbstractVisitor {
cls.add(AFlag.DONT_GENERATE);
return false;
}
- cls.markAnonymousClass();
removeSyntheticFields(cls);
cls.getMethods().forEach(ClassModifier::removeSyntheticMethods);
cls.getMethods().forEach(ClassModifier::removeEmptyMethods);
@@ -73,7 +73,7 @@ public class ClassModifier extends AbstractVisitor {
if (cls.getAccessFlags().isStatic()) {
return;
}
- boolean inline = cls.contains(AFlag.ANONYMOUS_CLASS);
+ boolean inline = cls.isAnonymous();
if (inline || cls.getClassInfo().isInner()) {
for (FieldNode field : cls.getFields()) {
if (field.getAccessFlags().isSynthetic() && field.getType().isObject()) {
diff --git a/jadx-core/src/main/java/jadx/core/dex/visitors/ModVisitor.java b/jadx-core/src/main/java/jadx/core/dex/visitors/ModVisitor.java
index f8bef6edd..ba1cfbd15 100644
--- a/jadx-core/src/main/java/jadx/core/dex/visitors/ModVisitor.java
+++ b/jadx-core/src/main/java/jadx/core/dex/visitors/ModVisitor.java
@@ -229,11 +229,8 @@ public class ModVisitor extends AbstractVisitor {
}
ClassNode classNode = callMthNode.getParentClass();
- if (!classNode.contains(AFlag.ANONYMOUS_CLASS)) {
- // check if class can be anonymous but not yet marked due to dependency issues
- if (!classNode.markAnonymousClass()) {
- return;
- }
+ if (!classNode.isAnonymous()) {
+ return;
}
if (!mth.getParentClass().getInnerClasses().contains(classNode)) {
return;
diff --git a/jadx-core/src/main/java/jadx/core/dex/visitors/ProcessAnonymous.java b/jadx-core/src/main/java/jadx/core/dex/visitors/ProcessAnonymous.java
new file mode 100644
index 000000000..faa66b823
--- /dev/null
+++ b/jadx-core/src/main/java/jadx/core/dex/visitors/ProcessAnonymous.java
@@ -0,0 +1,66 @@
+package jadx.core.dex.visitors;
+
+import jadx.core.dex.attributes.AFlag;
+import jadx.core.dex.nodes.ClassNode;
+import jadx.core.dex.nodes.FieldNode;
+import jadx.core.dex.nodes.MethodNode;
+import jadx.core.dex.nodes.RootNode;
+import jadx.core.dex.visitors.regions.RegionMakerVisitor;
+
+@JadxVisitor(
+ name = "ProcessAnonymous",
+ desc = "Mark anonymous and lambda classes (for future inline)",
+ runAfter = RegionMakerVisitor.class
+)
+public class ProcessAnonymous extends AbstractVisitor {
+
+ @Override
+ public void init(RootNode root) {
+ if (!root.getArgs().isInlineAnonymousClasses()) {
+ return;
+ }
+
+ for (ClassNode cls : root.getClasses(true)) {
+ markAnonymousClass(cls);
+ }
+ }
+
+ private static boolean markAnonymousClass(ClassNode cls) {
+ if (isAnonymous(cls) || isLambdaCls(cls)) {
+ cls.add(AFlag.ANONYMOUS_CLASS);
+ cls.add(AFlag.DONT_GENERATE);
+
+ for (MethodNode mth : cls.getMethods()) {
+ if (mth.isConstructor()) {
+ mth.add(AFlag.ANONYMOUS_CONSTRUCTOR);
+ }
+ }
+ return true;
+ }
+ return false;
+ }
+
+ private static boolean isAnonymous(ClassNode cls) {
+ return cls.getClassInfo().isInner()
+ && Character.isDigit(cls.getClassInfo().getShortName().charAt(0))
+ && cls.getMethods().stream().filter(MethodNode::isConstructor).count() == 1;
+ }
+
+ private static boolean isLambdaCls(ClassNode cls) {
+ return cls.getAccessFlags().isSynthetic()
+ && cls.getAccessFlags().isFinal()
+ && cls.getClassInfo().getRawName().contains(".-$$Lambda$")
+ && countStaticFields(cls) == 0;
+ }
+
+ private static int countStaticFields(ClassNode cls) {
+ int c = 0;
+ for (FieldNode field : cls.getFields()) {
+ if (field.getAccessFlags().isStatic()) {
+ c++;
+ }
+ }
+ return c;
+ }
+
+}
diff --git a/jadx-core/src/test/java/jadx/tests/integration/inner/TestAnonymousClass.java b/jadx-core/src/test/java/jadx/tests/integration/inner/TestAnonymousClass.java
index 7ace92742..ef2f05d00 100644
--- a/jadx-core/src/test/java/jadx/tests/integration/inner/TestAnonymousClass.java
+++ b/jadx-core/src/test/java/jadx/tests/integration/inner/TestAnonymousClass.java
@@ -8,6 +8,7 @@ import org.junit.jupiter.api.Test;
import jadx.core.dex.nodes.ClassNode;
import jadx.tests.api.IntegrationTest;
+import static jadx.tests.api.utils.JadxMatchers.containsOne;
import static org.hamcrest.CoreMatchers.containsString;
import static org.hamcrest.CoreMatchers.not;
import static org.hamcrest.MatcherAssert.assertThat;
@@ -37,5 +38,17 @@ public class TestAnonymousClass extends IntegrationTest {
assertThat(code, not(containsString("this")));
assertThat(code, not(containsString("null")));
assertThat(code, not(containsString("AnonymousClass_")));
+ assertThat(code, not(containsString("class AnonymousClass")));
+ }
+
+ @Test
+ public void testNoInline() {
+ getArgs().setInlineAnonymousClasses(false);
+
+ ClassNode cls = getClassNode(TestCls.class);
+ String code = cls.getCode().toString();
+
+ assertThat(code, containsString("class AnonymousClass1 implements FilenameFilter {"));
+ assertThat(code, containsOne("new AnonymousClass1()"));
}
}
diff --git a/jadx-gui/src/main/java/jadx/gui/settings/JadxSettings.java b/jadx-gui/src/main/java/jadx/gui/settings/JadxSettings.java
index 90d75401f..ca42113d7 100644
--- a/jadx-gui/src/main/java/jadx/gui/settings/JadxSettings.java
+++ b/jadx-gui/src/main/java/jadx/gui/settings/JadxSettings.java
@@ -1,9 +1,6 @@
package jadx.gui.settings;
-import java.awt.Font;
-import java.awt.GraphicsDevice;
-import java.awt.GraphicsEnvironment;
-import java.awt.Window;
+import java.awt.*;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
@@ -15,7 +12,7 @@ import java.util.Map;
import java.util.Set;
import java.util.function.Consumer;
-import javax.swing.JFrame;
+import javax.swing.*;
import org.fife.ui.rsyntaxtextarea.RSyntaxTextArea;
import org.jetbrains.annotations.Nullable;
@@ -281,6 +278,10 @@ public class JadxSettings extends JadxCLIArgs {
this.useImports = useImports;
}
+ public void setInlineAnonymousClasses(boolean inlineAnonymousClasses) {
+ this.inlineAnonymousClasses = inlineAnonymousClasses;
+ }
+
public boolean isAutoStartJobs() {
return autoStartJobs;
}
diff --git a/jadx-gui/src/main/java/jadx/gui/settings/JadxSettingsWindow.java b/jadx-gui/src/main/java/jadx/gui/settings/JadxSettingsWindow.java
index 1dc4c3343..d335edc79 100644
--- a/jadx-gui/src/main/java/jadx/gui/settings/JadxSettingsWindow.java
+++ b/jadx-gui/src/main/java/jadx/gui/settings/JadxSettingsWindow.java
@@ -347,6 +347,13 @@ public class JadxSettingsWindow extends JDialog {
needReload();
});
+ JCheckBox inlineAnonymous = new JCheckBox();
+ inlineAnonymous.setSelected(settings.isInlineAnonymousClasses());
+ inlineAnonymous.addItemListener(e -> {
+ settings.setInlineAnonymousClasses(e.getStateChange() == ItemEvent.SELECTED);
+ needReload();
+ });
+
SettingsGroup other = new SettingsGroup(NLS.str("preferences.decompile"));
other.addRow(NLS.str("preferences.threads"), threadsCount);
other.addRow(NLS.str("preferences.excludedPackages"), NLS.str("preferences.excludedPackages.tooltip"),
@@ -357,6 +364,7 @@ public class JadxSettingsWindow extends JDialog {
other.addRow(NLS.str("preferences.replaceConsts"), replaceConsts);
other.addRow(NLS.str("preferences.respectBytecodeAccessModifiers"), respectBytecodeAccessModifiers);
other.addRow(NLS.str("preferences.useImports"), useImports);
+ other.addRow(NLS.str("preferences.inlineAnonymous"), inlineAnonymous);
other.addRow(NLS.str("preferences.fallback"), fallback);
other.addRow(NLS.str("preferences.skipResourcesDecode"), resourceDecode);
return other;
diff --git a/jadx-gui/src/main/resources/i18n/Messages_en_US.properties b/jadx-gui/src/main/resources/i18n/Messages_en_US.properties
index ea1bb5524..bc1438ba5 100644
--- a/jadx-gui/src/main/resources/i18n/Messages_en_US.properties
+++ b/jadx-gui/src/main/resources/i18n/Messages_en_US.properties
@@ -90,6 +90,7 @@ preferences.escapeUnicode=Escape unicode
preferences.replaceConsts=Replace constants
preferences.respectBytecodeAccessModifiers=Respect bytecode access modifiers
preferences.useImports=Use import statements
+preferences.inlineAnonymous=Inline anonymous classes
preferences.skipResourcesDecode=Don't decode resources
preferences.autoSave=Auto save
preferences.threads=Processing threads count
diff --git a/jadx-gui/src/main/resources/i18n/Messages_es_ES.properties b/jadx-gui/src/main/resources/i18n/Messages_es_ES.properties
index 8f07b014d..81a460d69 100644
--- a/jadx-gui/src/main/resources/i18n/Messages_es_ES.properties
+++ b/jadx-gui/src/main/resources/i18n/Messages_es_ES.properties
@@ -90,6 +90,7 @@ preferences.escapeUnicode=Escape unicode
preferences.replaceConsts=Reemplazar constantes
#preferences.respectBytecodeAccessModifiers=
#preferences.useImports=
+#preferences.inlineAnonymous=
preferences.skipResourcesDecode=No descodificar recursos
#preferences.autoSave=
preferences.threads=Número de hilos a procesar
diff --git a/jadx-gui/src/main/resources/i18n/Messages_zh_CN.properties b/jadx-gui/src/main/resources/i18n/Messages_zh_CN.properties
index c1f5ffbc0..b6dc7fb83 100644
--- a/jadx-gui/src/main/resources/i18n/Messages_zh_CN.properties
+++ b/jadx-gui/src/main/resources/i18n/Messages_zh_CN.properties
@@ -90,6 +90,7 @@ preferences.escapeUnicode=将 Unicode 字符转义
preferences.replaceConsts=替换常量
preferences.respectBytecodeAccessModifiers=遵守字节码访问修饰符
preferences.useImports=使用 import 语句
+#preferences.inlineAnonymous=
preferences.skipResourcesDecode=不反编译资源文件
#preferences.autoSave=
preferences.threads=并行线程数