Compare commits

...

36 Commits

Author SHA1 Message Date
Skylot 63a571306c refactor: load resource table nodes in one change (#1648) 2022-08-19 22:15:04 +01:00
Skylot bc4db61e25 fix(gui): improve resources search (#1648) 2022-08-19 15:52:14 +01:00
Jan S c7e6e28830 fix(gui): improve log viewer dialog (#1311)(PR #1649)
* [gui]: improve log viewer dialog

* use method from UiUtils to set window icons

Co-authored-by: Skylot <skylot@gmail.com>
2022-08-18 19:55:41 +01:00
Skylot 1d7b6fdb2c fix(gui): additional checks on open search result (#1647) 2022-08-18 15:59:45 +01:00
Skylot ce5d8eeff8 fix: don't inline anonymous in self inner class (#1645) 2022-08-18 15:48:17 +01:00
Jan S 894e0e6132 fix: UnsupportedOperationException on adding a field (#1645)(PR #1646)
* fix: UnsupportedOperationException on adding a field

* changed list check and creation similar to safeAdd
2022-08-18 15:33:18 +01:00
Skylot 127f0ecf3f fix(gui): disable actions if files not loaded (#1644) 2022-08-16 21:28:57 +01:00
Skylot cf7767e702 fix(gui): handle null value in TableCellRenderer (#1642) 2022-08-16 20:48:23 +01:00
Skylot e0aedc7949 fix: improve top block search for try/catch (#1633) 2022-08-15 21:31:26 +01:00
Skylot bad78de74c perf: improve directory delete 2022-08-14 13:38:12 +01:00
Skylot 12df8a169f chore: update gradle and dependencies 2022-08-13 18:25:08 +01:00
Skylot 15c9d33339 fix(gui): handle possible classes overlap in disk cache (#1633) 2022-08-13 13:13:13 +00:00
Jiaxin Peng 7e0fafbaf1 fix(gui): fix broken FileDialog by using legacy sort (#1628)(PR #1630)
#1628 #1606 #1213 #1574 #1552
2022-08-11 13:59:46 +01:00
zhongqingsong 57b9c1dd7a fix(gui): update Messages_zh_CN.properties (PR #1627) 2022-08-11 13:29:15 +01:00
Skylot 8ba0c17259 fix: handle empty endless loop (#1611) 2022-08-10 22:07:52 +01:00
Areizen cd32151083 fix(gui): correct Frida snippet for constructor (PR #1605)
When hooking a constructor with Frida, call `$new` instead of `$init`. `$init` cannot be used to instantiate an object and is reserved for hooking.

Co-authored-by: Your Name <you@example.com>
2022-08-06 20:16:37 +01:00
Skylot 75b52d672e feat(gui): save project search history 2022-08-05 20:43:05 +01:00
Skylot 11d04508f7 feat(gui): add manual search, stop and sort actions to search dialog (#1600) 2022-08-05 20:09:33 +01:00
Skylot e641b773b5 fix(gui): improve search dialog performance 2022-08-05 14:53:48 +01:00
Skylot 6e5899c654 fix: checks for field init reorder (#1599) 2022-08-04 17:38:46 +01:00
Skylot c66ffaa7f9 feat(gui): show start page on jadx open 2022-08-03 16:44:55 +01:00
Skylot 5193c6a5d8 chore: add tool for automatically insert new i18n lines 2022-08-03 16:44:40 +01:00
Skylot e7212af547 chore: upgrade gradle wrapper to 7.5 2022-08-03 18:44:10 +03:00
FixTheBug 3ca1357af4 fix(gui): sort resources by deobfuscated name (#1595)(PR #1598)
Co-authored-by: /paul-nguyen-goldenowl <paul.nguyen.goldenowl@gmail.com>
2022-08-01 14:54:22 +01:00
Guilherme 90e95213e4 feat(gui): add Brazilian Portuguese translation (PR #1596)
* feat(translation): add pt-br
* fix: `adb_dialog` prefix deleted
2022-08-01 12:15:35 +01:00
Jan S ae2d4da585 fix(res): XML "@null" decoding (#1583)(PR #1594)
minor improvements
2022-07-31 13:50:32 +01:00
Skylot 691d5cd1e6 fix: hide unused label before exception handler in simple mode 2022-07-30 17:33:23 +01:00
Skylot 58a46c6417 fix(gui): add constructors usage into class usage (#1591) 2022-07-30 17:22:32 +01:00
Skylot d3f6160e62 feat: add option to disable finally block extraction (#1592) 2022-07-30 14:07:43 +01:00
Skylot 03e4afb12f fix: check variables before merge in finally block (#1592) 2022-07-30 13:48:53 +01:00
Hen_Ry 6802f6028e fix(gui): update german translation (PR #1554)
* german update
* Fix
* applied the latest changes as discussed

Co-authored-by: Jan Peter Stotz <jpstotz@users.noreply.github.com>
2022-07-27 17:45:36 +01:00
SiderealArt 5ca61cfe18 fix(gui): update zh-tw translation (PR #1589) 2022-07-27 11:16:11 +01:00
Julian Burner 32d55b48f2 fix(gui): replace mixed-up quotation marks with period (PR #1588) 2022-07-26 10:16:34 +01:00
Skylot ab4b6f9e54 feat: select better resource name (#1581) 2022-07-25 19:53:03 +01:00
Jan S 9100ad1220 fix(debugger): resolve NPE in adb device viewer (#1585) (PR #1586) 2022-07-25 17:44:55 +01:00
Skylot 8b4f8fb572 fix: resolve inherited method to use correct alias (#1582) 2022-07-24 19:15:52 +03:00
105 changed files with 3382 additions and 817 deletions
+5
View File
@@ -102,6 +102,7 @@ options:
--add-debug-lines - add comments with debug line numbers if available
--no-inline-anonymous - disable anonymous classes inline
--no-inline-methods - disable methods inline
--no-finally - don't extract finally block
--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
@@ -116,6 +117,10 @@ options:
'ignore' - don't read and don't save
--deobf-use-sourcename - use source file name as class name alias
--deobf-parse-kotlin-metadata - parse kotlin metadata to class and package names
--deobf-res-name-source - better name source for resources:
'auto' - automatically select best name (default)
'resources' - use resources names
'code' - use R class fields names
--use-kotlin-methods-for-var-names - use kotlin intrinsic methods to rename variables, values: disable, apply, apply-and-hide, default: apply
--rename-flags - fix options (comma-separated list of):
'case' - fix case sensitivity issues (according to --fs-case-sensitive option),
+4 -4
View File
@@ -1,6 +1,6 @@
plugins {
id 'com.github.ben-manes.versions' version '0.42.0'
id 'com.diffplug.spotless' version '6.8.0'
id 'com.diffplug.spotless' version '6.9.1'
}
ext.jadxVersion = System.getenv('JADX_VERSION') ?: "dev"
@@ -31,11 +31,11 @@ allprojects {
testImplementation 'ch.qos.logback:logback-classic:1.2.11'
testImplementation 'org.hamcrest:hamcrest-library:2.2'
testImplementation 'org.mockito:mockito-core:4.6.1'
testImplementation 'org.mockito:mockito-core:4.7.0'
testImplementation 'org.assertj:assertj-core:3.23.1'
testImplementation 'org.junit.jupiter:junit-jupiter-api:5.8.2'
testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.8.2'
testImplementation 'org.junit.jupiter:junit-jupiter-api:5.9.0'
testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.9.0'
testCompileOnly 'org.jetbrains:annotations:23.0.0'
}
Binary file not shown.
+2 -2
View File
@@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionSha256Sum=29e49b10984e585d8118b7d0bc452f944e386458df27371b49b4ac1dec4b7fda
distributionUrl=https\://services.gradle.org/distributions/gradle-7.4.2-bin.zip
distributionSha256Sum=f6b8596b10cce501591e92f229816aa4046424f3b24d771751b06779d58c8ec4
distributionUrl=https\://services.gradle.org/distributions/gradle-7.5.1-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
Vendored
+6
View File
@@ -205,6 +205,12 @@ set -- \
org.gradle.wrapper.GradleWrapperMain \
"$@"
# Stop when "xargs" is not available.
if ! command -v xargs >/dev/null 2>&1
then
echo "xargs is not available"
fi
# Use "xargs" to parse quoted args.
#
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
Vendored
+8 -6
View File
@@ -14,7 +14,7 @@
@rem limitations under the License.
@rem
@if "%DEBUG%" == "" @echo off
@if "%DEBUG%"=="" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@@ -25,7 +25,7 @@
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%" == "" set DIRNAME=.
if "%DIRNAME%"=="" set DIRNAME=.
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@@ -40,7 +40,7 @@ if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto execute
if %ERRORLEVEL% equ 0 goto execute
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
@@ -75,13 +75,15 @@ set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
:end
@rem End local scope for the variables with windows NT shell
if "%ERRORLEVEL%"=="0" goto mainEnd
if %ERRORLEVEL% equ 0 goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
exit /b 1
set EXIT_CODE=%ERRORLEVEL%
if %EXIT_CODE% equ 0 set EXIT_CODE=1
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
exit /b %EXIT_CODE%
:mainEnd
if "%OS%"=="Windows_NT" endlocal
@@ -21,6 +21,7 @@ import jadx.api.JadxArgs.RenameEnum;
import jadx.api.JadxArgs.UseKotlinMethodsForVarNames;
import jadx.api.JadxDecompiler;
import jadx.api.args.DeobfuscationMapFileMode;
import jadx.api.args.ResourceNameSource;
import jadx.core.utils.exceptions.JadxException;
import jadx.core.utils.files.FileUtils;
@@ -88,6 +89,9 @@ public class JadxCLIArgs {
@Parameter(names = { "--no-inline-methods" }, description = "disable methods inline")
protected boolean inlineMethods = true;
@Parameter(names = "--no-finally", description = "don't extract finally block")
protected boolean extractFinally = true;
@Parameter(names = "--no-replace-consts", description = "don't replace constant value with matching constant field")
protected boolean replaceConsts = true;
@@ -129,6 +133,16 @@ public class JadxCLIArgs {
@Parameter(names = { "--deobf-parse-kotlin-metadata" }, description = "parse kotlin metadata to class and package names")
protected boolean deobfuscationParseKotlinMetadata = false;
@Parameter(
names = { "--deobf-res-name-source" },
description = "better name source for resources:"
+ "\n 'auto' - automatically select best name (default)"
+ "\n 'resources' - use resources names"
+ "\n 'code' - use R class fields names",
converter = ResourceNameSourceConverter.class
)
protected ResourceNameSource resourceNameSource = ResourceNameSource.AUTO;
@Parameter(
names = { "--use-kotlin-methods-for-var-names" },
description = "use kotlin intrinsic methods to rename variables, values: disable, apply, apply-and-hide",
@@ -262,6 +276,7 @@ public class JadxCLIArgs {
args.setUseSourceNameAsClassAlias(deobfuscationUseSourceNameAsAlias);
args.setParseKotlinMetadata(deobfuscationParseKotlinMetadata);
args.setUseKotlinMethodsForVarNames(useKotlinMethodsForVarNames);
args.setResourceNameSource(resourceNameSource);
args.setEscapeUnicode(escapeUnicode);
args.setRespectBytecodeAccModifiers(respectBytecodeAccessModifiers);
args.setExportAsGradleProject(exportAsGradleProject);
@@ -270,6 +285,7 @@ public class JadxCLIArgs {
args.setInsertDebugLines(addDebugLines);
args.setInlineAnonymousClasses(inlineAnonymousClasses);
args.setInlineMethods(inlineMethods);
args.setExtractFinally(extractFinally);
args.setRenameFlags(renameFlags);
args.setFsCaseSensitive(fsCaseSensitive);
args.setCommentsLevel(commentsLevel);
@@ -350,6 +366,10 @@ public class JadxCLIArgs {
return inlineMethods;
}
public boolean isExtractFinally() {
return extractFinally;
}
public boolean isDeobfuscationOn() {
return deobfuscationOn;
}
@@ -378,6 +398,10 @@ public class JadxCLIArgs {
return deobfuscationParseKotlinMetadata;
}
public ResourceNameSource getResourceNameSource() {
return resourceNameSource;
}
public UseKotlinMethodsForVarNames getUseKotlinMethodsForVarNames() {
return useKotlinMethodsForVarNames;
}
@@ -502,6 +526,19 @@ public class JadxCLIArgs {
}
}
public static class ResourceNameSourceConverter implements IStringConverter<ResourceNameSource> {
@Override
public ResourceNameSource convert(String value) {
try {
return ResourceNameSource.valueOf(value.toUpperCase());
} catch (Exception e) {
throw new IllegalArgumentException(
'\'' + value + "' is unknown, possible values are: "
+ JadxCLIArgs.enumValuesString(ResourceNameSource.values()));
}
}
}
public static class DecompilationModeConverter implements IStringConverter<DecompilationMode> {
@Override
public DecompilationMode convert(String value) {
+3 -3
View File
@@ -5,11 +5,11 @@ plugins {
dependencies {
api(project(':jadx-plugins:jadx-plugins-api'))
implementation 'com.google.code.gson:gson:2.9.0'
implementation 'com.google.code.gson:gson:2.9.1'
// TODO: move resources decoding to separate plugin module
implementation 'com.android.tools.build:aapt2-proto:7.2.1-7984345'
implementation 'com.google.protobuf:protobuf-java:3.21.2' // forcing latest version
implementation 'com.android.tools.build:aapt2-proto:7.2.2-7984345'
implementation 'com.google.protobuf:protobuf-java:3.21.5' // forcing latest version
testImplementation 'org.apache.commons:commons-lang3:3.12.0'
@@ -16,6 +16,7 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import jadx.api.args.DeobfuscationMapFileMode;
import jadx.api.args.ResourceNameSource;
import jadx.api.data.ICodeData;
import jadx.api.impl.AnnotatedCodeWriter;
import jadx.api.impl.InMemoryCodeCache;
@@ -72,6 +73,7 @@ public class JadxArgs {
private File deobfuscationMapFile = null;
private DeobfuscationMapFileMode deobfuscationMapFileMode = DeobfuscationMapFileMode.READ;
private ResourceNameSource resourceNameSource = ResourceNameSource.AUTO;
private int deobfuscationMinLength = 0;
private int deobfuscationMaxLength = Integer.MAX_VALUE;
@@ -369,6 +371,14 @@ public class JadxArgs {
this.deobfuscationMapFile = deobfuscationMapFile;
}
public ResourceNameSource getResourceNameSource() {
return resourceNameSource;
}
public void setResourceNameSource(ResourceNameSource resourceNameSource) {
this.resourceNameSource = resourceNameSource;
}
public boolean isEscapeUnicode() {
return escapeUnicode;
}
@@ -540,6 +550,7 @@ public class JadxArgs {
String argStr = "args:" + decompilationMode + useImports + showInconsistentCode
+ inlineAnonymousClasses + inlineMethods
+ deobfuscationOn + deobfuscationMinLength + deobfuscationMaxLength
+ resourceNameSource
+ parseKotlinMetadata + useKotlinMethodsForVarNames
+ insertDebugLines + extractFinally
+ debugInfo + useSourceNameAsClassAlias + escapeUnicode + replaceConsts
@@ -564,6 +575,7 @@ public class JadxArgs {
+ ", deobfuscationOn=" + deobfuscationOn
+ ", deobfuscationMapFile=" + deobfuscationMapFile
+ ", deobfuscationMapFileMode=" + deobfuscationMapFileMode
+ ", resourceNameSource=" + resourceNameSource
+ ", useSourceNameAsClassAlias=" + useSourceNameAsClassAlias
+ ", parseKotlinMetadata=" + parseKotlinMetadata
+ ", useKotlinMethodsForVarNames=" + useKotlinMethodsForVarNames
@@ -0,0 +1,22 @@
package jadx.api.args;
/**
* Resources original name source (for deobfuscation)
*/
public enum ResourceNameSource {
/**
* Automatically select best name (default)
*/
AUTO,
/**
* Force use resources provided names
*/
RESOURCES,
/**
* Force use resources names from R class
*/
CODE,
}
@@ -191,7 +191,6 @@ public class Jadx {
passes.add(new ProcessInstructionsVisitor());
passes.add(new BlockSplitter());
passes.add(new MethodVisitor(mth -> mth.add(AFlag.DISABLE_BLOCKS_LOCK)));
if (args.isRawCFGOutput()) {
passes.add(DotGraphVisitor.dumpRaw());
}
@@ -215,9 +214,6 @@ public class Jadx {
passes.add(new CodeShrinkVisitor());
passes.add(new SimplifyVisitor());
passes.add(new MethodVisitor(mth -> mth.remove(AFlag.DONT_GENERATE)));
if (args.isRawCFGOutput()) {
passes.add(DotGraphVisitor.dumpRaw());
}
if (args.isCfgOutput()) {
passes.add(DotGraphVisitor.dump());
}
@@ -828,7 +828,11 @@ public class InsnGen {
if (insn.contains(AFlag.FORCE_RAW_NAME)) {
code.add(callMth.getName());
} else {
code.add(callMth.getAlias());
if (callMthNode != null) {
code.add(callMthNode.getAlias());
} else {
code.add(callMth.getAlias());
}
}
generateMethodArguments(code, insn, k, callMthNode);
}
@@ -53,7 +53,9 @@ public class SimpleModeHelper {
startLabel.set(block.getId());
} else if (predsCount == 1 && prev != null) {
if (!prev.equals(preds.get(0))) {
startLabel.set(block.getId());
if (!block.contains(AFlag.EXC_BOTTOM_SPLITTER)) {
startLabel.set(block.getId());
}
if (prev.getSuccessors().size() == 1 && !mth.isPreExitBlocks(prev)) {
endGoto.set(prev.getId());
}
@@ -428,7 +428,9 @@ public class Deobfuscator {
return "Enum";
}
String result = "";
if (cls.getAccessFlags().isAbstract()) {
if (cls.getAccessFlags().isInterface()) {
result += "Interface";
} else if (cls.getAccessFlags().isAbstract()) {
result += "Abstract";
}
@@ -91,6 +91,19 @@ public class LoopInfo {
this.parentLoop = parentLoop;
}
public boolean hasParent(LoopInfo searchLoop) {
LoopInfo parent = parentLoop;
while (true) {
if (parent == null) {
return false;
}
if (parent == searchLoop) {
return true;
}
parent = parent.getParentLoop();
}
}
@Override
public String toString() {
return "LOOP:" + id + ": " + start + "->" + end;
@@ -19,29 +19,59 @@ import static jadx.core.utils.Utils.lockList;
public final class BlockNode extends AttrNode implements IBlock, Comparable<BlockNode> {
/**
* Const ID
*/
private final int cid;
/**
* ID linked to position in blocks list (easier to use BitSet)
* TODO: rename to avoid confusion
*/
private int id;
/**
* Offset in methods bytecode
*/
private final int startOffset;
private final List<InsnNode> instructions = new ArrayList<>(2);
private List<BlockNode> predecessors = new ArrayList<>(1);
private List<BlockNode> successors = new ArrayList<>(1);
private List<BlockNode> cleanSuccessors;
// all dominators
/**
* All dominators, excluding self
*/
private BitSet doms = EmptyBitSet.EMPTY;
// dominance frontier
/**
* Dominance frontier
*/
private BitSet domFrontier;
// immediate dominator
/**
* Immediate dominator
*/
private BlockNode idom;
// blocks on which dominates this block
/**
* Blocks on which dominates this block
*/
private List<BlockNode> dominatesOn = new ArrayList<>(3);
public BlockNode(int id, int offset) {
public BlockNode(int cid, int id, int offset) {
this.cid = cid;
this.id = id;
this.startOffset = offset;
}
public void setId(int id) {
public int getCId() {
return cid;
}
void setId(int id) {
this.id = id;
}
@@ -188,12 +218,12 @@ public final class BlockNode extends AttrNode implements IBlock, Comparable<Bloc
return false;
}
BlockNode other = (BlockNode) obj;
return id == other.id && startOffset == other.startOffset;
return cid == other.cid && startOffset == other.startOffset;
}
@Override
public int compareTo(@NotNull BlockNode o) {
return Integer.compare(id, o.id);
return Integer.compare(cid, o.cid);
}
@Override
@@ -203,6 +233,6 @@ public final class BlockNode extends AttrNode implements IBlock, Comparable<Bloc
@Override
public String toString() {
return "B:" + id + ':' + InsnUtils.formatOffset(startOffset);
return "B:" + cid + ':' + InsnUtils.formatOffset(startOffset);
}
}
@@ -469,6 +469,9 @@ public class ClassNode extends NotificationAttrNode implements ILoadable, ICodeN
}
public void addField(FieldNode fld) {
if (fields == null || fields.isEmpty()) {
fields = new ArrayList<>(1);
}
fields.add(fld);
}
@@ -657,6 +660,10 @@ public class ClassNode extends NotificationAttrNode implements ILoadable, ICodeN
return contains(AType.ANONYMOUS_CLASS);
}
public boolean isSynthetic() {
return contains(AFlag.SYNTHETIC);
}
public boolean isInner() {
return parentClass != this;
}
@@ -57,6 +57,10 @@ public class FieldNode extends NotificationAttrNode implements ICodeNode {
return accFlags.isStatic();
}
public boolean isInstance() {
return !accFlags.isStatic();
}
public String getName() {
return fieldInfo.getName();
}
@@ -65,6 +69,10 @@ public class FieldNode extends NotificationAttrNode implements ICodeNode {
return fieldInfo.getAlias();
}
public void rename(String alias) {
fieldInfo.setAlias(alias);
}
public ArgType getType() {
return type;
}
@@ -73,6 +81,10 @@ public class FieldNode extends NotificationAttrNode implements ICodeNode {
return parentClass;
}
public ClassNode getTopParentClass() {
return parentClass.getTopParentClass();
}
public List<MethodNode> getUseIn() {
return useIn;
}
@@ -264,21 +264,6 @@ public class InsnNode extends LineAttrNode {
}
}
public boolean canReorderRecursive() {
if (!canReorder()) {
return false;
}
for (InsnArg arg : this.getArguments()) {
if (arg.isInsnWrap()) {
InsnNode wrapInsn = ((InsnWrapArg) arg).getWrapInsn();
if (!wrapInsn.canReorderRecursive()) {
return false;
}
}
}
return true;
}
public boolean containsWrappedInsn() {
for (InsnArg arg : this.getArguments()) {
if (arg.isInsnWrap()) {
@@ -61,6 +61,7 @@ public class MethodNode extends NotificationAttrNode implements IMethodDetails,
private List<RegisterArg> argsList;
private InsnNode[] instructions;
private List<BlockNode> blocks;
private int blocksMaxCId;
private BlockNode enterBlock;
private BlockNode exitBlock;
private List<SSAVar> sVars;
@@ -318,6 +319,15 @@ public class MethodNode extends NotificationAttrNode implements IMethodDetails,
public void setBasicBlocks(List<BlockNode> blocks) {
this.blocks = blocks;
int i = 0;
for (BlockNode block : blocks) {
block.setId(i);
i++;
}
}
public int getNextBlockCId() {
return blocksMaxCId++;
}
public BlockNode getEnterBlock() {
@@ -49,6 +49,10 @@ public final class LoopRegion extends ConditionRegion {
return header;
}
public boolean isEndless() {
return header == null;
}
public IRegion getBody() {
return body;
}
@@ -60,6 +60,10 @@ public class TryCatchBlockAttr implements IJadxAttribute {
return throwFound;
}
public int getId() {
return id;
}
public List<ExceptionHandler> getHandlers() {
return handlers;
}
@@ -100,7 +100,7 @@ public class DotGraphVisitor extends AbstractVisitor {
if (insnArr == null) {
return;
}
BlockNode block = new BlockNode(0, 0);
BlockNode block = new BlockNode(0, 0, 0);
List<InsnNode> insnList = block.getInstructions();
for (InsnNode insn : insnArr) {
if (insn != null) {
@@ -199,7 +199,7 @@ public class DotGraphVisitor extends AbstractVisitor {
dot.add("color=red,");
}
dot.add("label=\"{");
dot.add(String.valueOf(block.getId())).add("\\:\\ ");
dot.add(String.valueOf(block.getCId())).add("\\:\\ ");
dot.add(InsnUtils.formatOffset(block.getStartOffset()));
if (!attrs.isEmpty()) {
dot.add('|').add(attrs);
@@ -230,10 +230,10 @@ public class DotGraphVisitor extends AbstractVisitor {
if (PRINT_DOMINATORS) {
for (BlockNode c : block.getDominatesOn()) {
conn.startLine(block.getId() + " -> " + c.getId() + "[color=green];");
conn.startLine(block.getCId() + " -> " + c.getCId() + "[color=green];");
}
for (BlockNode dom : BlockUtils.bitSetToBlocks(mth, block.getDomFrontier())) {
conn.startLine("f_" + block.getId() + " -> f_" + dom.getId() + "[color=blue];");
conn.startLine("f_" + block.getCId() + " -> f_" + dom.getCId() + "[color=blue];");
}
}
}
@@ -273,7 +273,7 @@ public class DotGraphVisitor extends AbstractVisitor {
private String makeName(IContainer c) {
String name;
if (c instanceof BlockNode) {
name = "Node_" + ((BlockNode) c).getId();
name = "Node_" + ((BlockNode) c).getCId();
} else if (c instanceof IBlock) {
name = "Node_" + c.getClass().getSimpleName() + '_' + c.hashCode();
} else {
@@ -29,6 +29,7 @@ import jadx.core.dex.nodes.MethodNode;
import jadx.core.dex.visitors.shrink.CodeShrinkVisitor;
import jadx.core.utils.BlockUtils;
import jadx.core.utils.InsnRemover;
import jadx.core.utils.ListUtils;
import jadx.core.utils.Utils;
import jadx.core.utils.exceptions.JadxException;
@@ -45,20 +46,22 @@ public class ExtractFieldInit extends AbstractVisitor {
for (ClassNode inner : cls.getInnerClasses()) {
visit(inner);
}
moveStaticFieldsInit(cls);
moveCommonFieldsInit(cls);
if (!cls.getFields().isEmpty()) {
moveStaticFieldsInit(cls);
moveCommonFieldsInit(cls);
}
return false;
}
private static final class FieldInitInfo {
final FieldNode fieldNode;
final IndexInsnNode putInsn;
final boolean singlePath;
final boolean canMove;
public FieldInitInfo(FieldNode fieldNode, IndexInsnNode putInsn, boolean singlePath) {
public FieldInitInfo(FieldNode fieldNode, IndexInsnNode putInsn, boolean canMove) {
this.fieldNode = fieldNode;
this.putInsn = putInsn;
this.singlePath = singlePath;
this.canMove = canMove;
}
}
@@ -80,6 +83,9 @@ public class ExtractFieldInit extends AbstractVisitor {
|| classInitMth.getBasicBlocks() == null) {
return;
}
if (ListUtils.noneMatch(cls.getFields(), FieldNode::isStatic)) {
return;
}
while (processStaticFields(cls, classInitMth)) {
// sometimes instructions moved to field init prevent from vars inline -> inline and try again
CodeShrinkVisitor.shrinkMethod(classInitMth);
@@ -116,15 +122,15 @@ public class ExtractFieldInit extends AbstractVisitor {
}
private static void moveCommonFieldsInit(ClassNode cls) {
if (ListUtils.noneMatch(cls.getFields(), FieldNode::isInstance)) {
return;
}
List<MethodNode> constructors = getConstructorsList(cls);
if (constructors.isEmpty()) {
return;
}
List<ConstructorInitInfo> infoList = new ArrayList<>(constructors.size());
for (MethodNode constructorMth : constructors) {
if (constructorMth.isNoCode()) {
return;
}
List<FieldInitInfo> inits = collectFieldsInit(cls, constructorMth, InsnType.IPUT);
filterFieldsInit(inits);
if (inits.isEmpty()) {
@@ -168,19 +174,25 @@ public class ExtractFieldInit extends AbstractVisitor {
Set<BlockNode> singlePathBlocks = new HashSet<>();
BlockUtils.visitSinglePath(mth.getEnterBlock(), singlePathBlocks::add);
boolean canReorder = true;
for (BlockNode block : mth.getBasicBlocks()) {
for (InsnNode insn : block.getInstructions()) {
boolean fieldInsn = false;
if (insn.getType() == putType) {
IndexInsnNode putInsn = (IndexInsnNode) insn;
FieldInfo field = (FieldInfo) putInsn.getIndex();
if (field.getDeclClass().equals(cls.getClassInfo())) {
FieldNode fn = cls.searchField(field);
if (fn != null) {
boolean singlePath = singlePathBlocks.contains(block);
fieldsInit.add(new FieldInitInfo(fn, putInsn, singlePath));
boolean canMove = canReorder && singlePathBlocks.contains(block);
fieldsInit.add(new FieldInitInfo(fn, putInsn, canMove));
fieldInsn = true;
}
}
}
if (!fieldInsn && canReorder && !insn.canReorder()) {
canReorder = false;
}
}
}
return fieldsInit;
@@ -226,14 +238,14 @@ public class ExtractFieldInit extends AbstractVisitor {
}
private static boolean checkInsn(FieldInitInfo initInfo) {
if (!initInfo.singlePath) {
if (!initInfo.canMove) {
return false;
}
IndexInsnNode insn = initInfo.putInsn;
InsnArg arg = insn.getArg(0);
if (arg.isInsnWrap()) {
InsnNode wrapInsn = ((InsnWrapArg) arg).getWrapInsn();
if (!wrapInsn.canReorderRecursive() && insn.contains(AType.EXC_CATCH)) {
if (!wrapInsn.canReorder() && insn.contains(AType.EXC_CATCH)) {
return false;
}
} else {
@@ -364,7 +376,7 @@ public class ExtractFieldInit extends AbstractVisitor {
AccessInfo accFlags = mth.getAccessFlags();
if (!accFlags.isStatic() && accFlags.isConstructor()) {
list.add(mth);
if (BlockUtils.isAllBlocksEmpty(mth.getBasicBlocks())) {
if (mth.isNoCode() || BlockUtils.isAllBlocksEmpty(mth.getBasicBlocks())) {
return Collections.emptyList();
}
}
@@ -222,6 +222,10 @@ public class ProcessAnonymous extends AbstractVisitor {
// exclude self usage
return null;
}
if (ctrUseCls.getTopParentClass().equals(cls)) {
// exclude usage inside inner classes
return null;
}
for (MethodNode mth : cls.getMethods()) {
if (mth == ctr) {
continue;
@@ -171,10 +171,6 @@ public class BlockExceptionHandler {
}
}
protected static void removeTmpConnections(MethodNode mth) {
mth.getBasicBlocks().forEach(BlockExceptionHandler::removeTmpConnection);
}
private static void removeTmpConnection(BlockNode block) {
TmpEdgeAttr tmpEdgeAttr = block.get(AType.TMP_EDGE);
if (tmpEdgeAttr != null) {
@@ -402,6 +398,13 @@ public class BlockExceptionHandler {
}
BlockNode topDom = BlockUtils.getCommonDominator(mth, blocks);
if (topDom != null) {
// dominator always return one up block if blocks already contains dominator, use successor instead
if (topDom.getSuccessors().size() == 1) {
BlockNode upBlock = topDom.getSuccessors().get(0);
if (blocks.contains(upBlock)) {
return upBlock;
}
}
return adjustTopBlock(topDom);
}
throw new JadxRuntimeException("Failed to find top block for try-catch from: " + blocks);
@@ -137,8 +137,9 @@ public class BlockSplitter extends AbstractVisitor {
}
static BlockNode startNewBlock(MethodNode mth, int offset) {
BlockNode block = new BlockNode(mth.getBasicBlocks().size(), offset);
mth.getBasicBlocks().add(block);
List<BlockNode> blocks = mth.getBasicBlocks();
BlockNode block = new BlockNode(mth.getNextBlockCId(), blocks.size(), offset);
blocks.add(block);
return block;
}
@@ -391,7 +392,8 @@ public class BlockSplitter extends AbstractVisitor {
&& block.getSuccessors().size() <= 1
&& !block.getPredecessors().isEmpty()
&& !block.contains(AFlag.MTH_ENTER_BLOCK)
&& !block.contains(AFlag.MTH_EXIT_BLOCK);
&& !block.contains(AFlag.MTH_EXIT_BLOCK)
&& !block.getSuccessors().contains(block); // no self loop
}
static void collectSuccessors(BlockNode startBlock, BlockNode methodEnterBlock, Set<BlockNode> toRemove) {
@@ -25,23 +25,16 @@ public class DominatorTree {
List<BlockNode> sorted = sortBlocks(mth);
BlockNode[] doms = build(sorted);
apply(sorted, doms);
mth.setBasicBlocks(sorted);
}
private static List<BlockNode> sortBlocks(MethodNode mth) {
int blocksCount = mth.getBasicBlocks().size();
BitSet reachSet = new BitSet(blocksCount);
List<BlockNode> sorted = new ArrayList<>(blocksCount);
BlockUtils.dfsVisit(mth, b -> {
sorted.add(b);
reachSet.set(b.getId());
});
if (reachSet.cardinality() != blocksCount) {
BlockUtils.dfsVisit(mth, sorted::add);
if (sorted.size() != blocksCount) {
throw new JadxRuntimeException("Found unreachable blocks");
}
for (int i = 0; i < blocksCount; i++) {
sorted.get(i).setId(i);
}
mth.setBasicBlocks(sorted);
return sorted;
}
@@ -6,6 +6,7 @@ import java.util.List;
import java.util.Set;
import jadx.core.dex.nodes.BlockNode;
import jadx.core.dex.nodes.InsnNode;
import jadx.core.dex.nodes.MethodNode;
import jadx.core.dex.trycatch.ExceptionHandler;
import jadx.core.utils.Utils;
@@ -19,6 +20,10 @@ public class FinallyExtractInfo {
private final InsnsSlice finallyInsnsSlice = new InsnsSlice();
private final BlockNode startBlock;
private InsnsSlice curDupSlice;
private List<InsnNode> curDupInsns;
private int curDupInsnsOffset;
public FinallyExtractInfo(MethodNode mth, ExceptionHandler finallyHandler, BlockNode startBlock, List<BlockNode> allHandlerBlocks) {
this.mth = mth;
this.finallyHandler = finallyHandler;
@@ -54,6 +59,27 @@ public class FinallyExtractInfo {
return startBlock;
}
public InsnsSlice getCurDupSlice() {
return curDupSlice;
}
public void setCurDupSlice(InsnsSlice curDupSlice) {
this.curDupSlice = curDupSlice;
}
public List<InsnNode> getCurDupInsns() {
return curDupInsns;
}
public int getCurDupInsnsOffset() {
return curDupInsnsOffset;
}
public void setCurDupInsns(List<InsnNode> insns, int offset) {
this.curDupInsns = insns;
this.curDupInsnsOffset = offset;
}
@Override
public String toString() {
return "FinallyExtractInfo{"
@@ -12,6 +12,7 @@ import org.slf4j.LoggerFactory;
import jadx.core.Consts;
import jadx.core.dex.attributes.AFlag;
import jadx.core.dex.attributes.AType;
import jadx.core.dex.attributes.nodes.RegDebugInfoAttr;
import jadx.core.dex.instructions.InsnType;
import jadx.core.dex.instructions.args.InsnArg;
import jadx.core.dex.instructions.args.RegisterArg;
@@ -28,6 +29,7 @@ import jadx.core.dex.visitors.IDexTreeVisitor;
import jadx.core.dex.visitors.JadxVisitor;
import jadx.core.dex.visitors.ssa.SSATransform;
import jadx.core.utils.BlockUtils;
import jadx.core.utils.InsnList;
import jadx.core.utils.ListUtils;
import jadx.core.utils.Utils;
@@ -376,6 +378,7 @@ public class MarkFinallyVisitor extends AbstractVisitor {
* 'Finally' instructions can start in the middle of the first block.
*/
private static InsnsSlice isStartBlock(BlockNode dupBlock, BlockNode finallyBlock, FinallyExtractInfo extractInfo) {
extractInfo.setCurDupSlice(null);
List<InsnNode> dupInsns = dupBlock.getInstructions();
List<InsnNode> finallyInsns = finallyBlock.getInstructions();
int dupSize = dupInsns.size();
@@ -386,7 +389,7 @@ public class MarkFinallyVisitor extends AbstractVisitor {
int startPos;
int endPos = 0;
if (dupSize == finSize) {
if (!checkInsns(dupInsns, finallyInsns, 0)) {
if (!checkInsns(extractInfo, dupInsns, finallyInsns, 0)) {
return null;
}
startPos = 0;
@@ -394,11 +397,11 @@ public class MarkFinallyVisitor extends AbstractVisitor {
// dupSize > finSize
startPos = dupSize - finSize;
// fast check from end of block
if (!checkInsns(dupInsns, finallyInsns, startPos)) {
if (!checkInsns(extractInfo, dupInsns, finallyInsns, startPos)) {
// search start insn
boolean found = false;
for (int i = 1; i < startPos; i++) {
if (checkInsns(dupInsns, finallyInsns, i)) {
if (checkInsns(extractInfo, dupInsns, finallyInsns, i)) {
startPos = i;
endPos = finSize + i;
found = true;
@@ -414,6 +417,7 @@ public class MarkFinallyVisitor extends AbstractVisitor {
// put instructions into slices
boolean complete;
InsnsSlice slice = new InsnsSlice();
extractInfo.setCurDupSlice(slice);
int endIndex;
if (endPos != 0) {
endIndex = endPos + 1;
@@ -453,11 +457,12 @@ public class MarkFinallyVisitor extends AbstractVisitor {
return slice;
}
private static boolean checkInsns(List<InsnNode> remInsns, List<InsnNode> finallyInsns, int delta) {
private static boolean checkInsns(FinallyExtractInfo extractInfo, List<InsnNode> dupInsns, List<InsnNode> finallyInsns, int delta) {
extractInfo.setCurDupInsns(dupInsns, delta);
for (int i = finallyInsns.size() - 1; i >= 0; i--) {
InsnNode startInsn = finallyInsns.get(i);
InsnNode remInsn = remInsns.get(delta + i);
if (!sameInsns(remInsn, startInsn)) {
InsnNode dupInsn = dupInsns.get(delta + i);
if (!sameInsns(extractInfo, dupInsn, startInsn)) {
return false;
}
}
@@ -509,8 +514,9 @@ public class MarkFinallyVisitor extends AbstractVisitor {
if (dupInsnCount < finallyInsnCount) {
return false;
}
extractInfo.setCurDupInsns(dupInsns, 0);
for (int i = 0; i < finallyInsnCount; i++) {
if (!sameInsns(dupInsns.get(i), finallyInsns.get(i))) {
if (!sameInsns(extractInfo, dupInsns.get(i), finallyInsns.get(i))) {
return false;
}
}
@@ -524,26 +530,85 @@ public class MarkFinallyVisitor extends AbstractVisitor {
return true;
}
private static boolean sameInsns(InsnNode remInsn, InsnNode fInsn) {
if (!remInsn.isSame(fInsn)) {
private static boolean sameInsns(FinallyExtractInfo extractInfo, InsnNode dupInsn, InsnNode fInsn) {
if (!dupInsn.isSame(fInsn)) {
return false;
}
// TODO: check instance arg in ConstructorInsn
// TODO: compare literals
for (int i = 0; i < remInsn.getArgsCount(); i++) {
InsnArg remArg = remInsn.getArg(i);
for (int i = 0; i < dupInsn.getArgsCount(); i++) {
InsnArg dupArg = dupInsn.getArg(i);
InsnArg fArg = fInsn.getArg(i);
if (remArg.isRegister() != fArg.isRegister()) {
if (!isSameArgs(extractInfo, dupArg, fArg)) {
return false;
}
boolean remConst = remArg.isConst();
if (remConst != fArg.isConst()) {
return false;
}
if (remConst && !remArg.isSameConst(fArg)) {
}
return true;
}
@SuppressWarnings("RedundantIfStatement")
private static boolean isSameArgs(FinallyExtractInfo extractInfo, InsnArg dupArg, InsnArg fArg) {
boolean isReg = dupArg.isRegister();
if (isReg != fArg.isRegister()) {
return false;
}
if (isReg) {
RegisterArg dupReg = (RegisterArg) dupArg;
RegisterArg fReg = (RegisterArg) fArg;
if (!dupReg.sameCodeVar(fReg)
&& !sameDebugInfo(dupReg, fReg)
&& assignedOutsideHandler(extractInfo, dupReg, fReg)
&& assignInsnDifferent(dupReg, fReg)) {
return false;
}
}
boolean remConst = dupArg.isConst();
if (remConst != fArg.isConst()) {
return false;
}
if (remConst && !dupArg.isSameConst(fArg)) {
return false;
}
return true;
}
private static boolean sameDebugInfo(RegisterArg dupReg, RegisterArg fReg) {
RegDebugInfoAttr fDbgInfo = fReg.get(AType.REG_DEBUG_INFO);
RegDebugInfoAttr dupDbgInfo = dupReg.get(AType.REG_DEBUG_INFO);
if (fDbgInfo == null || dupDbgInfo == null) {
return false;
}
return dupDbgInfo.equals(fDbgInfo);
}
private static boolean assignInsnDifferent(RegisterArg dupReg, RegisterArg fReg) {
InsnNode assignInsn = fReg.getAssignInsn();
InsnNode dupAssign = dupReg.getAssignInsn();
if (assignInsn == null || dupAssign == null) {
return true;
}
if (!assignInsn.isSame(dupAssign)) {
return true;
}
if (assignInsn.isConstInsn() && dupAssign.isConstInsn()) {
return !assignInsn.isDeepEquals(dupAssign);
}
return false;
}
@SuppressWarnings("RedundantIfStatement")
private static boolean assignedOutsideHandler(FinallyExtractInfo extractInfo, RegisterArg dupReg, RegisterArg fReg) {
if (InsnList.contains(extractInfo.getFinallyInsnsSlice().getInsnsList(), fReg.getAssignInsn())) {
return false;
}
InsnNode dupAssign = dupReg.getAssignInsn();
InsnsSlice curDupSlice = extractInfo.getCurDupSlice();
if (curDupSlice != null && InsnList.contains(curDupSlice.getInsnsList(), dupAssign)) {
return false;
}
List<InsnNode> curDupInsns = extractInfo.getCurDupInsns();
if (Utils.notEmpty(curDupInsns) && InsnList.contains(curDupInsns, dupAssign, extractInfo.getCurDupInsnsOffset())) {
return false;
}
return true;
}
@@ -8,6 +8,7 @@ import jadx.core.dex.nodes.IContainer;
import jadx.core.dex.nodes.IRegion;
import jadx.core.dex.nodes.MethodNode;
import jadx.core.dex.regions.Region;
import jadx.core.dex.regions.loops.LoopRegion;
import jadx.core.dex.visitors.AbstractVisitor;
public class CleanRegions extends AbstractVisitor {
@@ -42,6 +43,13 @@ public class CleanRegions extends AbstractVisitor {
BlockNode block = (BlockNode) container;
return block.getInstructions().isEmpty();
}
if (container instanceof LoopRegion) {
LoopRegion loopRegion = (LoopRegion) container;
if (loopRegion.isEndless()) {
// keep empty endless loops
return false;
}
}
if (container instanceof IRegion) {
List<IContainer> subBlocks = ((IRegion) container).getSubBlocks();
for (IContainer subBlock : subBlocks) {
@@ -464,7 +464,7 @@ public class RegionMaker {
BlockNode exitEnd = BlockUtils.followEmptyPath(exit);
List<LoopInfo> loops = exitEnd.getAll(AType.LOOP);
for (LoopInfo loopAtEnd : loops) {
if (loopAtEnd != loop) {
if (loopAtEnd != loop && loop.hasParent(loopAtEnd)) {
insertEdge = exitEdge;
confirm = true;
break;
@@ -0,0 +1,62 @@
package jadx.core.utils;
import java.util.HashSet;
import java.util.Locale;
import java.util.Objects;
import java.util.Set;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import jadx.core.deobf.NameMapper;
import jadx.core.deobf.TldHelper;
public class BetterName {
private static final Logger LOG = LoggerFactory.getLogger(BetterName.class);
private static final boolean DEBUG = true;
public static String compareAndGet(String first, String second) {
if (Objects.equals(first, second)) {
return first;
}
int firstRating = calcRating(first);
int secondRating = calcRating(second);
boolean firstBetter = firstRating >= secondRating;
if (DEBUG) {
if (firstBetter) {
LOG.info("Better name: '{}' > '{}' ({} > {})", first, second, firstRating, secondRating);
} else {
LOG.info("Better name: '{}' > '{}' ({} > {})", second, first, secondRating, firstRating);
}
}
return firstBetter ? first : second;
}
public static int calcRating(String str) {
int rating = str.length() * 3;
rating += differentCharsCount(str) * 20;
if (NameMapper.isAllCharsPrintable(str)) {
rating += 100;
}
if (NameMapper.isValidIdentifier(str)) {
rating += 50;
}
if (TldHelper.contains(str)) {
rating += 20;
}
if (str.contains("_")) {
// rare in obfuscated names
rating += 100;
}
return rating;
}
private static int differentCharsCount(String str) {
String lower = str.toLowerCase(Locale.ROOT);
Set<Integer> chars = new HashSet<>();
StringUtils.visitCodePoints(lower, chars::add);
return chars.size();
}
}
@@ -697,7 +697,7 @@ public class BlockUtils {
}
/**
* Search lowest common ancestor in dominator tree for input set.
* Search the lowest common ancestor in dominator tree for input set.
*/
@Nullable
public static BlockNode getCommonDominator(MethodNode mth, List<BlockNode> blocks) {
@@ -29,8 +29,12 @@ public final class InsnList implements Iterable<InsnNode> {
}
public static int getIndex(List<InsnNode> list, InsnNode insn) {
return getIndex(list, insn, 0);
}
public static int getIndex(List<InsnNode> list, InsnNode insn, int startOffset) {
int size = list.size();
for (int i = 0; i < size; i++) {
for (int i = startOffset; i < size; i++) {
if (list.get(i) == insn) {
return i;
}
@@ -38,6 +42,14 @@ public final class InsnList implements Iterable<InsnNode> {
return -1;
}
public static boolean contains(List<InsnNode> list, InsnNode insn) {
return getIndex(list, insn, 0) != -1;
}
public static boolean contains(List<InsnNode> list, InsnNode insn, int startOffset) {
return getIndex(list, insn, startOffset) != -1;
}
public int getIndex(InsnNode insn) {
return getIndex(list, insn);
}
@@ -160,6 +160,10 @@ public class ListUtils {
return true;
}
public static <T> boolean noneMatch(Collection<T> list, Predicate<T> test) {
return !anyMatch(list, test);
}
public static <T> boolean anyMatch(Collection<T> list, Predicate<T> test) {
if (list == null || list.isEmpty()) {
return false;
@@ -22,6 +22,7 @@ import jadx.core.dex.nodes.IRegion;
import jadx.core.dex.nodes.InsnNode;
import jadx.core.dex.nodes.MethodNode;
import jadx.core.dex.regions.Region;
import jadx.core.dex.regions.loops.LoopRegion;
import jadx.core.dex.trycatch.CatchAttr;
import jadx.core.dex.trycatch.ExceptionHandler;
import jadx.core.dex.trycatch.TryCatchBlockAttr;
@@ -289,7 +290,11 @@ public class RegionUtils {
}
}
return false;
} else if (container instanceof IRegion) {
}
if (container instanceof LoopRegion) {
return true;
}
if (container instanceof IRegion) {
IRegion region = (IRegion) container;
for (IContainer block : region.getSubBlocks()) {
if (notEmpty(block)) {
@@ -297,9 +302,8 @@ public class RegionUtils {
}
}
return false;
} else {
throw new JadxRuntimeException(unknownContainerType(container));
}
throw new JadxRuntimeException(unknownContainerType(container));
}
public static void getAllRegionBlocks(IContainer container, Set<IBlock> blocks) {
@@ -129,7 +129,7 @@ public class AndroidResourcesUtils {
FieldNode newResField = new FieldNode(typeCls, rFieldInfo,
AccessFlags.PUBLIC | AccessFlags.STATIC | AccessFlags.FINAL);
newResField.addAttr(new EncodedValue(EncodedType.ENCODED_INT, resource.getId()));
typeCls.getFields().add(newResField);
typeCls.addField(newResField);
if (rClsExists) {
newResField.addInfoComment("Added by JADX");
}
@@ -10,12 +10,16 @@ import java.io.InputStream;
import java.io.OutputStream;
import java.nio.charset.StandardCharsets;
import java.nio.file.FileVisitOption;
import java.nio.file.FileVisitResult;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.StandardOpenOption;
import java.nio.file.attribute.BasicFileAttributes;
import java.security.MessageDigest;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.jar.JarEntry;
@@ -108,13 +112,8 @@ public class FileUtils {
}
public static boolean deleteDir(File dir) {
File[] content = dir.listFiles();
if (content != null) {
for (File file : content) {
deleteDir(file);
}
}
return dir.delete();
deleteDir(dir.toPath());
return true;
}
public static void deleteDirIfExists(Path dir) {
@@ -127,16 +126,23 @@ public class FileUtils {
}
}
private static final SimpleFileVisitor<Path> FILE_DELETE_VISITOR = new SimpleFileVisitor<Path>() {
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
Files.delete(file);
return FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
Files.delete(dir);
return FileVisitResult.CONTINUE;
}
};
private static void deleteDir(Path dir) {
try (Stream<Path> pathStream = Files.walk(dir)) {
pathStream.sorted(Comparator.reverseOrder())
.forEach(path -> {
try {
Files.delete(path);
} catch (Exception e) {
throw new JadxRuntimeException("Failed to delete path: " + path.toAbsolutePath(), e);
}
});
try {
Files.walkFileTree(dir, Collections.emptySet(), Integer.MAX_VALUE, FILE_DELETE_VISITOR);
} catch (Exception e) {
throw new JadxRuntimeException("Failed to delete directory " + dir, e);
}
@@ -228,7 +234,8 @@ public class FileUtils {
public static void writeFile(Path file, String data) throws IOException {
FileUtils.makeDirsForFile(file);
Files.write(file, data.getBytes(StandardCharsets.UTF_8));
Files.write(file, data.getBytes(StandardCharsets.UTF_8),
StandardOpenOption.WRITE, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING);
}
public static String readFile(Path textFile) throws IOException {
@@ -10,14 +10,18 @@ import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.jetbrains.annotations.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import jadx.api.ICodeInfo;
import jadx.api.args.ResourceNameSource;
import jadx.core.deobf.NameMapper;
import jadx.core.dex.attributes.AFlag;
import jadx.core.dex.nodes.FieldNode;
import jadx.core.dex.nodes.RootNode;
import jadx.core.utils.BetterName;
import jadx.core.utils.exceptions.JadxRuntimeException;
import jadx.core.xmlgen.entry.EntryConfig;
import jadx.core.xmlgen.entry.RawNamedValue;
import jadx.core.xmlgen.entry.RawValue;
@@ -287,35 +291,66 @@ public class ResTableParser extends CommonBinaryParser implements IResParser {
if (renamedKey != null) {
return renamedKey;
}
FieldNode constField = root.getConstValues().getGlobalConstFields().get(resRef);
if (constField != null) {
constField.add(AFlag.DONT_RENAME);
return constField.getName();
}
// styles might contain dots in name, use VALID_RES_KEY_PATTERN only for resource file name
// styles might contain dots in name, search for alias only for resources names
if (typeName.equals("style")) {
return origKeyName;
} else if (VALID_RES_KEY_PATTERN.matcher(origKeyName).matches()) {
return origKeyName;
}
FieldNode constField = root.getConstValues().getGlobalConstFields().get(resRef);
String resAlias = getResAlias(resRef, origKeyName, constField);
resStorage.addRename(resRef, resAlias);
if (constField != null) {
constField.rename(resAlias);
constField.add(AFlag.DONT_RENAME);
}
return resAlias;
}
private String getResAlias(int resRef, String origKeyName, @Nullable FieldNode constField) {
String name;
if (constField == null || constField.getTopParentClass().isSynthetic()) {
name = origKeyName;
} else {
name = getBetterName(root.getArgs().getResourceNameSource(), origKeyName, constField.getName());
}
Matcher matcher = VALID_RES_KEY_PATTERN.matcher(name);
if (matcher.matches()) {
return name;
}
// Making sure origKeyName compliant with resource file name rules
Matcher m = VALID_RES_KEY_PATTERN.matcher(origKeyName);
String cleanedResName = cleanName(matcher);
String newResName = String.format("res_0x%08x", resRef);
if (cleanedResName.isEmpty()) {
return newResName;
}
// autogenerate key name, appended with cleaned origKeyName to be human-friendly
return newResName + "_" + cleanedResName.toLowerCase();
}
public static String getBetterName(ResourceNameSource nameSource, String resName, String codeName) {
switch (nameSource) {
case AUTO:
return BetterName.compareAndGet(resName, codeName);
case RESOURCES:
return resName;
case CODE:
return codeName;
default:
throw new JadxRuntimeException("Unexpected ResourceNameSource value: " + nameSource);
}
}
private String cleanName(Matcher matcher) {
StringBuilder sb = new StringBuilder();
boolean first = true;
while (m.find()) {
while (matcher.find()) {
if (!first) {
sb.append("_");
}
sb.append(m.group());
sb.append(matcher.group());
first = false;
}
// autogenerate key name, appended with cleaned origKeyName to be human-friendly
String newResName = String.format("res_0x%08x", resRef);
String cleanedResName = sb.toString();
if (!cleanedResName.isEmpty()) {
newResName += "_" + cleanedResName.toLowerCase();
}
return newResName;
return sb.toString();
}
private RawNamedValue parseValueMap() throws IOException {
@@ -168,11 +168,13 @@ public class ResXmlGen {
private void addItem(ICodeWriter cw, String itemTag, String typeName, RawNamedValue value) {
String nameStr = vp.decodeNameRef(value.getNameRef());
String valueStr = vp.decodeValue(value.getRawValue());
int dataType = value.getRawValue().getDataType();
if (!typeName.equals("attr")) {
if (valueStr == null || valueStr.equals("0")) {
if (dataType == ParserConstants.TYPE_REFERENCE && (valueStr == null || valueStr.equals("0"))) {
valueStr = "@null";
}
if (nameStr != null) {
if (dataType == ParserConstants.TYPE_INT_DEC && nameStr != null) {
try {
int intVal = Integer.parseInt(valueStr);
String newVal = ManifestAttributes.getInstance().decode(nameStr.replace("android:attr.", ""), intVal);
@@ -0,0 +1,22 @@
package jadx.core.utils;
import org.junit.jupiter.api.Test;
import static jadx.core.utils.BetterName.calcRating;
import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat;
public class TestBetterName {
@Test
public void test() {
expectFirst("color_main", "t0");
expectFirst("done", "oOo0oO0o");
}
private void expectFirst(String first, String second) {
String best = BetterName.compareAndGet(first, second);
assertThat(best)
.as(() -> String.format("'%s'=%d, '%s'=%d", first, calcRating(first), second, calcRating(second)))
.isEqualTo(first);
}
}
@@ -555,7 +555,7 @@ public abstract class IntegrationTest extends TestUtils {
protected void enableDeobfuscation() {
args.setDeobfuscationOn(true);
args.setDeobfuscationMapFileMode(DeobfuscationMapFileMode.OVERWRITE);
args.setDeobfuscationMapFileMode(DeobfuscationMapFileMode.IGNORE);
args.setDeobfuscationMinLength(2);
args.setDeobfuscationMaxLength(64);
}
@@ -113,7 +113,7 @@ public class TestCompiler implements Closeable {
assertNotNull(mth, "Failed to get method " + methodName + '(' + Arrays.toString(types) + ')');
return mth.invoke(inst, args);
} catch (Throwable e) {
IntegrationTest.rethrow("Invoke error", e);
IntegrationTest.rethrow("Invoke error for method: " + methodName, e);
return null;
}
}
@@ -0,0 +1,41 @@
package jadx.tests.integration.deobf;
import org.junit.jupiter.api.Test;
import jadx.tests.api.IntegrationTest;
import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat;
public class TestInheritedMethodRename extends IntegrationTest {
public static class TestCls {
public static class A extends B {
}
public static class B {
public void call() {
System.out.println("call");
}
}
public void test(A a) {
// reference to A.call() not renamed,
// should be resolved to B.call() and use alias
a.call();
}
}
@Test
public void test() {
noDebugInfo();
enableDeobfuscation();
getArgs().setDeobfuscationMinLength(99);
assertThat(getClassNode(TestCls.class))
.code()
.containsOne("public void m0call() {")
.doesNotContain(".call();")
.containsOne(".m0call();");
}
}
@@ -0,0 +1,39 @@
package jadx.tests.integration.inner;
import org.junit.jupiter.api.Test;
import jadx.api.JadxInternalAccess;
import jadx.api.JavaClass;
import jadx.core.dex.attributes.AType;
import jadx.core.dex.nodes.ClassNode;
import jadx.tests.api.IntegrationTest;
import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat;
public class TestAnonymousClass20 extends IntegrationTest {
@SuppressWarnings({ "unused", "checkstyle:TypeName", "Convert2Lambda", "Anonymous2MethodRef" })
public static class Test$Cls {
public Runnable test() {
return new Runnable() {
@Override
public void run() {
new Test$Cls();
}
};
}
}
@Test
public void test() {
ClassNode cls = getClassNode(Test$Cls.class);
assertThat(cls.get(AType.ANONYMOUS_CLASS)).isNull();
JavaClass javaClass = JadxInternalAccess.convertClassNode(jadxDecompiler, cls);
assertThat(javaClass.getTopParentClass()).isEqualTo(javaClass);
assertThat(cls)
.code()
.containsOne("new TestAnonymousClass20$Test$Cls();");
}
}
@@ -0,0 +1,20 @@
package jadx.tests.integration.loops;
import org.junit.jupiter.api.Test;
import jadx.tests.api.SmaliTest;
import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat;
/**
* Empty endless loop, issue #1611
*/
public class TestEndlessLoop2 extends SmaliTest {
@Test
public void test() {
disableCompilation();
assertThat(getClassNodeFromSmali())
.code()
.countString(2, "while (true) {");
}
}
@@ -1,7 +1,5 @@
package jadx.tests.integration.names;
import java.util.List;
import org.junit.jupiter.api.Test;
import jadx.api.CommentsLevel;
@@ -0,0 +1,49 @@
package jadx.tests.integration.others;
import org.junit.jupiter.api.Test;
import jadx.tests.api.IntegrationTest;
import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat;
/**
* Negative case for field initialization move (#1599).
* Can't reorder with other instance methods.
*/
public class TestFieldInitNegative extends IntegrationTest {
public static class TestCls {
StringBuilder sb;
int field;
public TestCls() {
initBuilder(new StringBuilder("sb"));
this.field = initField();
this.sb.append(this.field);
}
private void initBuilder(StringBuilder sb) {
this.sb = sb;
}
private int initField() {
return sb.length();
}
public String getStr() {
return sb.toString();
}
public void check() {
assertThat(new TestCls().getStr()).isEqualTo("sb2"); // no NPE
}
}
@Test
public void test() {
assertThat(getClassNode(TestCls.class))
.code()
.doesNotContain("int field = initField();")
.containsOne("this.field = initField();");
}
}
@@ -0,0 +1,18 @@
package jadx.tests.integration.trycatch;
import org.junit.jupiter.api.Test;
import jadx.tests.api.SmaliTest;
import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat;
public class TestNestedTryCatch4 extends SmaliTest {
@Test
public void test() {
disableCompilation();
assertThat(getClassNodeFromSmali())
.code()
.doesNotContain("?? ");
}
}
@@ -0,0 +1,44 @@
package jadx.tests.integration.trycatch;
import org.junit.jupiter.api.Test;
import jadx.tests.api.SmaliTest;
import static jadx.tests.api.utils.assertj.JadxAssertions.assertThat;
/**
* Negative test case for finally extract (issue 1592).
* Different registers incorrectly merged into one.
*/
@SuppressWarnings({ "CommentedOutCode", "GrazieInspection" })
public class TestTryCatchFinally15 extends SmaliTest {
// @formatter:off
/*
protected final Parcel test(int i, Parcel parcel) throws RemoteException {
Parcel obtain = Parcel.obtain();
try {
try {
this.zza.transact(i, parcel, obtain, 0);
obtain.readException();
return obtain;
} catch (RuntimeException e) {
obtain.recycle();
throw e;
}
} finally {
parcel.recycle();
}
}
*/
// @formatter:on
@Test
public void test() {
disableCompilation();
assertThat(getClassNodeFromSmali())
.code()
.doesNotContain("parcel = Parcel.obtain();")
.containsOne("this.zza.transact(i, parcel, obtain, 0);");
}
}
@@ -10,6 +10,7 @@ import jadx.core.dex.nodes.ClassNode;
import jadx.tests.api.IntegrationTest;
import static jadx.tests.api.utils.JadxMatchers.containsLines;
import static jadx.tests.api.utils.JadxMatchers.containsOne;
import static org.hamcrest.MatcherAssert.assertThat;
public class TestTryCatchFinally6 extends IntegrationTest {
@@ -54,15 +55,7 @@ public class TestTryCatchFinally6 extends IntegrationTest {
ClassNode cls = getClassNode(TestCls.class);
String code = cls.getCode().toString();
assertThat(code, containsLines(2,
"FileInputStream fileInputStream = null;",
"try {",
indent() + "call();",
indent() + "fileInputStream = new FileInputStream(\"1.txt\");",
"} finally {",
indent() + "if (fileInputStream != null) {",
indent() + indent() + "fileInputStream.close();",
indent() + '}',
"}"));
// impossible to proof that variables should be merged, so can't restore finally block here
assertThat(code, containsOne("if (0 != 0) {"));
}
}
@@ -0,0 +1,90 @@
.class Lloops/TestEndlessLoop2;
.super Ljava/lang/Object;
.field instanceCount:J
.method test([Ljava/lang/String;)V
.registers 10
const/16 p1, 0xb
invoke-virtual {p0, p1}, Lloops/TestEndlessLoop2;->vMeth(I)V
const/16 v0, 0xf1
const-wide/high16 v1, 0x4032000000000000L # 18.0
:goto_a
const-wide/high16 v3, 0x4076000000000000L # 352.0
const/4 v5, 0x1
cmpg-double v6, v1, v3
if-gez v6, :cond_1c
const/4 v0, 0x1
:goto_12
add-int/2addr v0, v5
const/16 v3, 0x4b
if-ge v0, v3, :cond_18
goto :goto_12
:cond_18
const-wide/high16 v3, 0x3ff0000000000000L # 1.0
add-double/2addr v1, v3
goto :goto_a
:cond_1c
iget-wide v3, p0, Lloops/TestEndlessLoop2;->instanceCount:J
long-to-int v4, v3
const/16 v3, 0xb
:goto_21
const/16 v6, 0xf3
if-ge v5, v6, :cond_41
rem-int/lit8 v6, v5, 0x9
add-int/lit8 v6, v6, 0x12
if-eq v6, p1, :cond_3e
const/16 v7, 0x15
if-eq v6, v7, :cond_36
const/16 v7, 0x16
if-eq v6, v7, :cond_34
goto :goto_3b
:cond_34
add-int/2addr v4, v0
goto :goto_3b
:cond_36
const v6, 0xeed9
div-int/2addr v3, v6
nop
:goto_3b
add-int/lit8 v5, v5, 0x1
goto :goto_21
:cond_3e
nop
:goto_3f
nop
# endless loop with empty body
goto :goto_3f
:cond_41
sget-object p1, Ljava/lang/System;->out:Ljava/io/PrintStream;
invoke-static {v1, v2}, Ljava/lang/Double;->doubleToLongBits(D)J
move-result-wide v0
new-instance v2, Ljava/lang/StringBuilder;
invoke-direct {v2}, Ljava/lang/StringBuilder;-><init>()V
const-string v5, "i21 d2 i22 = "
invoke-virtual {v2, v5}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;
invoke-virtual {v2, v3}, Ljava/lang/StringBuilder;->append(I)Ljava/lang/StringBuilder;
const-string v3, ","
invoke-virtual {v2, v3}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;
invoke-virtual {v2, v0, v1}, Ljava/lang/StringBuilder;->append(J)Ljava/lang/StringBuilder;
invoke-virtual {v2, v3}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;
invoke-virtual {v2, v4}, Ljava/lang/StringBuilder;->append(I)Ljava/lang/StringBuilder;
invoke-virtual {v2}, Ljava/lang/StringBuilder;->toString()Ljava/lang/String;
move-result-object v0
invoke-virtual {p1, v0}, Ljava/io/PrintStream;->println(Ljava/lang/String;)V
return-void
.end method
@@ -0,0 +1,601 @@
.class public Ltrycatch/TestNestedTryCatch4;
.super Landroid/app/NativeActivity;
.method private test(Landroid/content/Intent;)V
.registers 11
.annotation system Ldalvik/annotation/MethodParameters;
accessFlags = {
0x0
}
names = {
"intent"
}
.end annotation
const-string v0, "IOException while closing input stream\n"
if-nez p1, :cond_5
return-void
:cond_5
const-string v1, "intent_cmd"
.line 1740
invoke-virtual {p1, v1}, Landroid/content/Intent;->getStringExtra(Ljava/lang/String;)Ljava/lang/String;
move-result-object v1
const-string v2, "MCPE"
if-eqz v1, :cond_80
.line 1741
invoke-virtual {v1}, Ljava/lang/String;->length()I
move-result v3
if-lez v3, :cond_80
.line 1743
:try_start_15
new-instance p1, Lorg/json/JSONObject;
invoke-direct {p1, v1}, Lorg/json/JSONObject;-><init>(Ljava/lang/String;)V
const-string v0, "Command"
.line 1744
invoke-virtual {p1, v0}, Lorg/json/JSONObject;->getString(Ljava/lang/String;)Ljava/lang/String;
move-result-object v0
const-string v1, "keyboardResult"
.line 1745
invoke-virtual {v0, v1}, Ljava/lang/String;->equals(Ljava/lang/Object;)Z
move-result v1
if-eqz v1, :cond_33
const-string v0, "Text"
.line 1746
invoke-virtual {p1, v0}, Lorg/json/JSONObject;->getString(Ljava/lang/String;)Ljava/lang/String;
move-result-object p1
invoke-virtual {p0, p1}, Ltrycatch/TestNestedTryCatch4;->nativeSetTextboxText(Ljava/lang/String;)V
goto/16 :goto_208
:cond_33
const-string v1, "fileDialogResult"
.line 1748
invoke-virtual {v0, v1}, Ljava/lang/String;->equals(Ljava/lang/Object;)Z
move-result v0
if-eqz v0, :cond_208
iget-wide v0, p0, Ltrycatch/TestNestedTryCatch4;->mFileDialogCallback:J
const-wide/16 v3, 0x0
cmp-long v5, v0, v3
if-eqz v5, :cond_208
const-string v0, "Result"
.line 1749
invoke-virtual {p1, v0}, Lorg/json/JSONObject;->getString(Ljava/lang/String;)Ljava/lang/String;
move-result-object v0
const-string v1, "Ok"
invoke-virtual {v0, v1}, Ljava/lang/String;->equals(Ljava/lang/Object;)Z
move-result v0
if-eqz v0, :cond_5d
.line 1750
iget-wide v0, p0, Ltrycatch/TestNestedTryCatch4;->mFileDialogCallback:J
const-string v5, "Path"
invoke-virtual {p1, v5}, Lorg/json/JSONObject;->getString(Ljava/lang/String;)Ljava/lang/String;
move-result-object p1
invoke-virtual {p0, v0, v1, p1}, Ltrycatch/TestNestedTryCatch4;->nativeOnPickImageSuccess(JLjava/lang/String;)V
goto :goto_62
.line 1753
:cond_5d
iget-wide v0, p0, Ltrycatch/TestNestedTryCatch4;->mFileDialogCallback:J
invoke-virtual {p0, v0, v1}, Ltrycatch/TestNestedTryCatch4;->nativeOnPickImageCanceled(J)V
.line 1755
:goto_62
iput-wide v3, p0, Ltrycatch/TestNestedTryCatch4;->mFileDialogCallback:J
:try_end_64
.catch Lorg/json/JSONException; {:try_start_15 .. :try_end_64} :catch_66
goto/16 :goto_208
:catch_66
move-exception p1
.line 1759
new-instance v0, Ljava/lang/StringBuilder;
invoke-direct {v0}, Ljava/lang/StringBuilder;-><init>()V
const-string v1, "JSONObject exception:"
invoke-virtual {v0, v1}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;
invoke-virtual {p1}, Lorg/json/JSONException;->toString()Ljava/lang/String;
move-result-object p1
invoke-virtual {v0, p1}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;
invoke-virtual {v0}, Ljava/lang/StringBuilder;->toString()Ljava/lang/String;
move-result-object p1
invoke-static {v2, p1}, Landroid/util/Log;->d(Ljava/lang/String;Ljava/lang/String;)I
return-void
.line 1765
:cond_80
invoke-virtual {p1}, Landroid/content/Intent;->getAction()Ljava/lang/String;
move-result-object v1
.line 1766
invoke-virtual {p1}, Landroid/content/Intent;->getType()Ljava/lang/String;
const-string/jumbo v3, "xbox_live_game_invite"
.line 1768
invoke-virtual {v3, v1}, Ljava/lang/String;->equals(Ljava/lang/Object;)Z
move-result v3
if-eqz v3, :cond_b0
const-string/jumbo v0, "xbl"
.line 1771
invoke-virtual {p1, v0}, Landroid/content/Intent;->getStringExtra(Ljava/lang/String;)Ljava/lang/String;
move-result-object p1
.line 1772
new-instance v0, Ljava/lang/StringBuilder;
invoke-direct {v0}, Ljava/lang/StringBuilder;-><init>()V
const-string v3, "[XboxLive] Received Invite "
invoke-virtual {v0, v3}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;
invoke-virtual {v0, p1}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;
invoke-virtual {v0}, Ljava/lang/StringBuilder;->toString()Ljava/lang/String;
move-result-object v0
invoke-static {v2, v0}, Landroid/util/Log;->d(Ljava/lang/String;Ljava/lang/String;)I
.line 1776
invoke-virtual {p0, v1, p1}, Ltrycatch/TestNestedTryCatch4;->nativeProcessIntentUriQuery(Ljava/lang/String;Ljava/lang/String;)V
goto/16 :goto_208
:cond_b0
const-string v3, "android.intent.action.VIEW"
.line 1779
invoke-virtual {v3, v1}, Ljava/lang/String;->equals(Ljava/lang/Object;)Z
move-result v3
if-nez v3, :cond_c0
const-string v3, "org.chromium.arc.intent.action.VIEW"
invoke-virtual {v3, v1}, Ljava/lang/String;->equals(Ljava/lang/Object;)Z
move-result v1
if-eqz v1, :cond_208
.line 1780
:cond_c0
invoke-virtual {p1}, Landroid/content/Intent;->getScheme()Ljava/lang/String;
move-result-object v1
.line 1781
invoke-virtual {p1}, Landroid/content/Intent;->getData()Landroid/net/Uri;
move-result-object p1
if-nez p1, :cond_cb
return-void
:cond_cb
const-string v3, "minecraft"
.line 1787
invoke-virtual {v3, v1}, Ljava/lang/String;->equalsIgnoreCase(Ljava/lang/String;)Z
move-result v3
if-nez v3, :cond_1f9
const-string v3, "minecraftedu"
invoke-virtual {v3, v1}, Ljava/lang/String;->equalsIgnoreCase(Ljava/lang/String;)Z
move-result v3
if-eqz v3, :cond_dd
goto/16 :goto_1f9
:cond_dd
const-string v3, "file"
.line 1795
invoke-virtual {v3, v1}, Ljava/lang/String;->equalsIgnoreCase(Ljava/lang/String;)Z
move-result v3
const-string v4, "&"
if-eqz v3, :cond_108
.line 1798
new-instance v0, Ljava/lang/StringBuilder;
invoke-direct {v0}, Ljava/lang/StringBuilder;-><init>()V
invoke-virtual {p1}, Landroid/net/Uri;->getPath()Ljava/lang/String;
move-result-object v1
invoke-virtual {v0, v1}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;
invoke-virtual {v0, v4}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;
invoke-virtual {p1}, Landroid/net/Uri;->getPath()Ljava/lang/String;
move-result-object p1
invoke-virtual {v0, p1}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;
invoke-virtual {v0}, Ljava/lang/StringBuilder;->toString()Ljava/lang/String;
move-result-object p1
const-string v0, "fileIntent"
invoke-virtual {p0, v0, p1}, Ltrycatch/TestNestedTryCatch4;->nativeProcessIntentUriQuery(Ljava/lang/String;Ljava/lang/String;)V
goto/16 :goto_208
:cond_108
const-string v3, "content"
.line 1800
invoke-virtual {v3, v1}, Ljava/lang/String;->equalsIgnoreCase(Ljava/lang/String;)Z
move-result v1
if-eqz v1, :cond_208
.line 1803
new-instance v1, Ljava/io/File;
invoke-virtual {p1}, Landroid/net/Uri;->getPath()Ljava/lang/String;
move-result-object v3
invoke-direct {v1, v3}, Ljava/io/File;-><init>(Ljava/lang/String;)V
invoke-virtual {v1}, Ljava/io/File;->getName()Ljava/lang/String;
move-result-object v1
.line 1804
new-instance v3, Ljava/io/File;
new-instance v5, Ljava/lang/StringBuilder;
invoke-direct {v5}, Ljava/lang/StringBuilder;-><init>()V
invoke-virtual {p0}, Ltrycatch/TestNestedTryCatch4;->getApplicationContext()Landroid/content/Context;
move-result-object v6
invoke-virtual {v6}, Landroid/content/Context;->getCacheDir()Ljava/io/File;
move-result-object v6
invoke-virtual {v5, v6}, Ljava/lang/StringBuilder;->append(Ljava/lang/Object;)Ljava/lang/StringBuilder;
const-string v6, "/"
invoke-virtual {v5, v6}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;
invoke-virtual {v5, v1}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;
invoke-virtual {v5}, Ljava/lang/StringBuilder;->toString()Ljava/lang/String;
move-result-object v1
invoke-direct {v3, v1}, Ljava/io/File;-><init>(Ljava/lang/String;)V
.line 1806
invoke-virtual {p0}, Ltrycatch/TestNestedTryCatch4;->getContentResolver()Landroid/content/ContentResolver;
move-result-object v1
.line 1810
:try_start_142
invoke-virtual {v1, p1}, Landroid/content/ContentResolver;->openInputStream(Landroid/net/Uri;)Ljava/io/InputStream;
move-result-object v1
:try_end_146
.catch Ljava/io/IOException; {:try_start_142 .. :try_end_146} :catch_1df
.line 1818
:try_start_146
new-instance v5, Ljava/io/FileOutputStream;
invoke-direct {v5, v3}, Ljava/io/FileOutputStream;-><init>(Ljava/io/File;)V
const/high16 v6, 0x100000
new-array v6, v6, [B
.line 1824
:goto_14f
invoke-virtual {v1, v6}, Ljava/io/InputStream;->read([B)I
move-result v7
const/4 v8, -0x1
if-eq v7, v8, :cond_15b
const/4 v8, 0x0
.line 1825
invoke-virtual {v5, v6, v8, v7}, Ljava/io/OutputStream;->write([BII)V
goto :goto_14f
.line 1828
:cond_15b
invoke-virtual {v5}, Ljava/io/OutputStream;->close()V
const-string v5, "contentIntent"
.line 1831
new-instance v6, Ljava/lang/StringBuilder;
invoke-direct {v6}, Ljava/lang/StringBuilder;-><init>()V
invoke-virtual {p1}, Landroid/net/Uri;->getPath()Ljava/lang/String;
move-result-object p1
invoke-virtual {v6, p1}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;
invoke-virtual {v6, v4}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;
invoke-virtual {v3}, Ljava/io/File;->getAbsolutePath()Ljava/lang/String;
move-result-object p1
invoke-virtual {v6, p1}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;
invoke-virtual {v6}, Ljava/lang/StringBuilder;->toString()Ljava/lang/String;
move-result-object p1
invoke-virtual {p0, v5, p1}, Ltrycatch/TestNestedTryCatch4;->nativeProcessIntentUriQuery(Ljava/lang/String;Ljava/lang/String;)V
:try_end_17d
.catch Ljava/io/IOException; {:try_start_146 .. :try_end_17d} :catch_18b
.catchall {:try_start_146 .. :try_end_17d} :catchall_189
.line 1842
:try_start_17d
invoke-virtual {v1}, Ljava/io/InputStream;->close()V
:try_end_180
.catch Ljava/io/IOException; {:try_start_17d .. :try_end_180} :catch_182
goto/16 :goto_208
:catch_182
move-exception p1
.line 1845
new-instance v1, Ljava/lang/StringBuilder;
invoke-direct {v1}, Ljava/lang/StringBuilder;-><init>()V
goto :goto_1b1
:catchall_189
move-exception p1
goto :goto_1c3
:catch_18b
move-exception p1
.line 1833
:try_start_18c
new-instance v4, Ljava/lang/StringBuilder;
invoke-direct {v4}, Ljava/lang/StringBuilder;-><init>()V
const-string v5, "IOException while copying file from content intent\n"
invoke-virtual {v4, v5}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;
invoke-virtual {p1}, Ljava/io/IOException;->toString()Ljava/lang/String;
move-result-object p1
invoke-virtual {v4, p1}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;
invoke-virtual {v4}, Ljava/lang/StringBuilder;->toString()Ljava/lang/String;
move-result-object p1
invoke-static {v2, p1}, Landroid/util/Log;->e(Ljava/lang/String;Ljava/lang/String;)I
:try_end_1a4
.catchall {:try_start_18c .. :try_end_1a4} :catchall_189
.line 1837
:try_start_1a4
invoke-virtual {v3}, Ljava/io/File;->delete()Z
:try_end_1a7
.catch Ljava/lang/Exception; {:try_start_1a4 .. :try_end_1a7} :catch_1a7
.catchall {:try_start_1a4 .. :try_end_1a7} :catchall_189
.line 1842
:catch_1a7
:try_start_1a7
invoke-virtual {v1}, Ljava/io/InputStream;->close()V
:try_end_1aa
.catch Ljava/io/IOException; {:try_start_1a7 .. :try_end_1aa} :catch_1ab
goto :goto_208
:catch_1ab
move-exception p1
.line 1845
new-instance v1, Ljava/lang/StringBuilder;
invoke-direct {v1}, Ljava/lang/StringBuilder;-><init>()V
:goto_1b1
invoke-virtual {v1, v0}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;
invoke-virtual {p1}, Ljava/io/IOException;->toString()Ljava/lang/String;
move-result-object p1
invoke-virtual {v1, p1}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;
invoke-virtual {v1}, Ljava/lang/StringBuilder;->toString()Ljava/lang/String;
move-result-object p1
invoke-static {v2, p1}, Landroid/util/Log;->e(Ljava/lang/String;Ljava/lang/String;)I
goto :goto_208
.line 1842
:goto_1c3
:try_start_1c3
invoke-virtual {v1}, Ljava/io/InputStream;->close()V
:try_end_1c6
.catch Ljava/io/IOException; {:try_start_1c3 .. :try_end_1c6} :catch_1c7
goto :goto_1de
:catch_1c7
move-exception v1
.line 1845
new-instance v3, Ljava/lang/StringBuilder;
invoke-direct {v3}, Ljava/lang/StringBuilder;-><init>()V
invoke-virtual {v3, v0}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;
invoke-virtual {v1}, Ljava/io/IOException;->toString()Ljava/lang/String;
move-result-object v0
invoke-virtual {v3, v0}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;
invoke-virtual {v3}, Ljava/lang/StringBuilder;->toString()Ljava/lang/String;
move-result-object v0
invoke-static {v2, v0}, Landroid/util/Log;->e(Ljava/lang/String;Ljava/lang/String;)I
.line 1847
:goto_1de
throw p1
:catch_1df
move-exception p1
.line 1813
new-instance v0, Ljava/lang/StringBuilder;
invoke-direct {v0}, Ljava/lang/StringBuilder;-><init>()V
const-string v1, "IOException while opening file from content intent\n"
invoke-virtual {v0, v1}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;
invoke-virtual {p1}, Ljava/io/IOException;->toString()Ljava/lang/String;
move-result-object p1
invoke-virtual {v0, p1}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;
invoke-virtual {v0}, Ljava/lang/StringBuilder;->toString()Ljava/lang/String;
move-result-object p1
invoke-static {v2, p1}, Landroid/util/Log;->e(Ljava/lang/String;Ljava/lang/String;)I
return-void
.line 1788
:cond_1f9
:goto_1f9
invoke-virtual {p1}, Landroid/net/Uri;->getHost()Ljava/lang/String;
move-result-object v0
.line 1789
invoke-virtual {p1}, Landroid/net/Uri;->getQuery()Ljava/lang/String;
move-result-object p1
if-nez v0, :cond_205
if-eqz p1, :cond_208
.line 1792
:cond_205
invoke-virtual {p0, v0, p1}, Ltrycatch/TestNestedTryCatch4;->nativeProcessIntentUriQuery(Ljava/lang/String;Ljava/lang/String;)V
:cond_208
:goto_208
return-void
.end method
@@ -0,0 +1,60 @@
.class public Ltrycatch/TestTryCatchFinally15;
.super Ljava/lang/Object;
.implements Landroid/os/IInterface;
.field private final zza:Landroid/os/IBinder;
.field private final zzb:Ljava/lang/String;
.method protected final test(ILandroid/os/Parcel;)Landroid/os/Parcel;
.registers 6
.annotation system Ldalvik/annotation/Throws;
value = {
Landroid/os/RemoteException;
}
.end annotation
.line 1
invoke-static {}, Landroid/os/Parcel;->obtain()Landroid/os/Parcel;
move-result-object v0
:try_start_4
iget-object v1, p0, Ltrycatch/TestTryCatchFinally15;->zza:Landroid/os/IBinder;
const/4 v2, 0x0
.line 2
invoke-interface {v1, p1, p2, v0, v2}, Landroid/os/IBinder;->transact(ILandroid/os/Parcel;Landroid/os/Parcel;I)Z
.line 3
invoke-virtual {v0}, Landroid/os/Parcel;->readException()V
:try_end_d
.catch Ljava/lang/RuntimeException; {:try_start_4 .. :try_end_d} :catch_13
.catchall {:try_start_4 .. :try_end_d} :catchall_11
.line 6
invoke-virtual {p2}, Landroid/os/Parcel;->recycle()V
return-object v0
:catchall_11
move-exception p1
goto :goto_18
.line 5
:catch_13
move-exception p1
.line 4
:try_start_14
invoke-virtual {v0}, Landroid/os/Parcel;->recycle()V
.line 5
throw p1
:try_end_18
.catchall {:try_start_14 .. :try_end_18} :catchall_11
.line 6
:goto_18
invoke-virtual {p2}, Landroid/os/Parcel;->recycle()V
.line 7
throw p1
.end method
+9 -3
View File
@@ -20,13 +20,13 @@ dependencies {
implementation 'com.formdev:flatlaf-extras:2.4'
implementation 'com.formdev:svgSalamander:1.1.3'
implementation 'com.google.code.gson:gson:2.9.0'
implementation 'com.google.code.gson:gson:2.9.1'
implementation 'org.apache.commons:commons-lang3:3.12.0'
implementation 'org.apache.commons:commons-text:1.9'
implementation 'io.reactivex.rxjava2:rxjava:2.2.21'
implementation "com.github.akarnokd:rxjava2-swing:0.3.7"
implementation 'com.android.tools.build:apksig:7.2.1'
implementation 'com.android.tools.build:apksig:7.2.2'
implementation 'io.github.hqktech:jdwp:1.0'
// TODO: Switch back to upstream once this PR gets merged:
@@ -42,7 +42,8 @@ application {
mainClass.set('jadx.gui.JadxGUI')
// The option -XX:+UseG1GC is only relevant for Java 8. Starting with Java 9 G1GC is already the default GC
applicationDefaultJvmArgs = ['-Xms128M', '-XX:MaxRAMPercentage=70.0', '-XX:+UseG1GC',
'-Dawt.useSystemAAFontSettings=lcd', '-Dswing.aatext=true']
'-Dawt.useSystemAAFontSettings=lcd', '-Dswing.aatext=true',
'-Djava.util.Arrays.useLegacyMergeSort=true']
}
applicationDistribution.with {
@@ -138,3 +139,8 @@ task distWinWithJre(type: Zip, dependsOn: ['copyDistWinWithJre']) {
}
duplicatesStrategy = DuplicatesStrategy.EXCLUDE
}
task addNewNLSLines(type: JavaExec) {
classpath = sourceSets.main.runtimeClasspath
mainClass.set('jadx.gui.utils.tools.NLSAddNewLines')
}
@@ -103,7 +103,7 @@ public class ADB {
}
return result;
} catch (SocketException e) {
LOG.error("Aborting readServiceProtocol: socket closed");
LOG.warn("Aborting readServiceProtocol: {}", e.toString());
} catch (IOException e) {
LOG.error("Failed to read readServiceProtocol", e);
}
@@ -207,7 +207,7 @@ public class ADB {
List<ADBDeviceInfo> deviceInfoList = new ArrayList<>(deviceLines.length);
for (String deviceLine : deviceLines) {
if (!deviceLine.trim().isEmpty()) {
deviceInfoList.add(ADBDeviceInfo.make(deviceLine, host, port));
deviceInfoList.add(new ADBDeviceInfo(deviceLine, host, port));
}
}
listener.onDeviceStatusChange(deviceInfoList);
@@ -39,7 +39,10 @@ public class ADBDevice {
}
public boolean updateDeviceInfo(ADBDeviceInfo info) {
boolean matched = this.info.serial.equals(info.serial);
if (info.getSerial() == null || info.getSerial().isEmpty()) {
return false;
}
boolean matched = this.info.getSerial().equals(info.getSerial());
if (matched) {
this.info = info;
}
@@ -47,21 +50,21 @@ public class ADBDevice {
}
public String getSerial() {
return info.serial;
return info.getSerial();
}
public boolean removeForward(String localPort) throws IOException {
return ADB.removeForward(info.adbHost, info.adbPort, info.serial, localPort);
return ADB.removeForward(info.getAdbHost(), info.getAdbPort(), info.getSerial(), localPort);
}
public ForwardResult forwardJDWP(String localPort, String jdwpPid) throws IOException {
try (Socket socket = ADB.connect(info.adbHost, info.adbPort)) {
try (Socket socket = ADB.connect(info.getAdbHost(), info.getAdbPort())) {
String cmd = String.format("host:forward:tcp:%s;jdwp:%s", localPort, jdwpPid);
cmd = String.format("%04x%s", cmd.length(), cmd);
InputStream inputStream = socket.getInputStream();
OutputStream outputStream = socket.getOutputStream();
ForwardResult rst;
if (ADB.setSerial(info.serial, outputStream, inputStream)) {
if (ADB.setSerial(info.getSerial(), outputStream, inputStream)) {
outputStream.write(cmd.getBytes());
if (!ADB.isOkay(inputStream)) {
rst = new ForwardResult(1, ADB.readServiceProtocol(inputStream));
@@ -99,9 +102,9 @@ public class ADBDevice {
*/
public int launchApp(String fullAppName) throws IOException, InterruptedException {
byte[] res;
try (Socket socket = ADB.connect(info.adbHost, info.adbPort)) {
try (Socket socket = ADB.connect(info.getAdbHost(), info.getAdbPort())) {
String cmd = "am start -D -n " + fullAppName;
res = ADB.execShellCommandRaw(info.serial, cmd, socket.getOutputStream(), socket.getInputStream());
res = ADB.execShellCommandRaw(info.getSerial(), cmd, socket.getOutputStream(), socket.getInputStream());
if (res == null) {
return -1;
}
@@ -134,13 +137,13 @@ public class ADBDevice {
}
public List<String> getProp(String entry) throws IOException {
try (Socket socket = ADB.connect(info.adbHost, info.adbPort)) {
try (Socket socket = ADB.connect(info.getAdbHost(), info.getAdbPort())) {
List<String> props = Collections.emptyList();
String cmd = "getprop";
if (!StringUtils.isEmpty(entry)) {
cmd += " " + entry;
}
byte[] payload = ADB.execShellCommandRaw(info.serial, cmd,
byte[] payload = ADB.execShellCommandRaw(info.getSerial(), cmd,
socket.getOutputStream(), socket.getInputStream());
if (payload != null) {
props = new ArrayList<>();
@@ -166,9 +169,9 @@ public class ADBDevice {
}
private List<Process> getProcessList(String cmd, int index) throws IOException {
try (Socket socket = ADB.connect(info.adbHost, info.adbPort)) {
try (Socket socket = ADB.connect(info.getAdbHost(), info.getAdbPort())) {
List<Process> procs = new ArrayList<>();
byte[] payload = ADB.execShellCommandRaw(info.serial, cmd,
byte[] payload = ADB.execShellCommandRaw(info.getSerial(), cmd,
socket.getOutputStream(), socket.getInputStream());
if (payload != null) {
String ps = new String(payload);
@@ -194,10 +197,10 @@ public class ADBDevice {
if (this.jdwpListenerSock != null) {
return false;
}
jdwpListenerSock = ADB.connect(this.info.adbHost, this.info.adbPort);
jdwpListenerSock = ADB.connect(this.info.getAdbHost(), this.info.getAdbPort());
InputStream inputStream = jdwpListenerSock.getInputStream();
OutputStream outputStream = jdwpListenerSock.getOutputStream();
if (ADB.setSerial(info.serial, outputStream, inputStream)
if (ADB.setSerial(info.getSerial(), outputStream, inputStream)
&& ADB.execCommandAsync(outputStream, inputStream, CMD_TRACK_JDWP)) {
Executors.newFixedThreadPool(1).execute(() -> {
for (;;) {
@@ -244,19 +247,20 @@ public class ADBDevice {
@Override
public int hashCode() {
return info.serial.hashCode();
return info.getSerial().hashCode();
}
@Override
public boolean equals(Object obj) {
if (obj instanceof ADBDevice) {
return ((ADBDevice) obj).getDeviceInfo().serial.equals(info.serial);
String otherSerial = ((ADBDevice) obj).getDeviceInfo().getSerial();
return otherSerial.equals(info.getSerial());
}
return false;
}
@Override
public String toString() {
return info.allInfo;
return info.getAllInfo();
}
}
@@ -1,42 +1,91 @@
package jadx.gui.device.protocol;
import java.util.Map;
import java.util.TreeMap;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import jadx.core.utils.log.LogUtils;
public class ADBDeviceInfo {
public String adbHost;
public int adbPort;
public String serial;
public String state;
public String model;
public String allInfo;
private static final Logger LOG = LoggerFactory.getLogger(ADBDeviceInfo.class);
private final String adbHost;
private final int adbPort;
private final String serial;
private final String state;
private final String model;
private final String allInfo;
/**
* Store the device info property values like "device" "model" "product" or "transport_id"
*/
private final Map<String, String> propertiesMap = new TreeMap<>();
ADBDeviceInfo(String info, String host, int port) {
String[] infoFields = info.trim().split("\\s+");
allInfo = String.join(" ", infoFields);
if (infoFields.length > 2) {
serial = infoFields[0];
state = infoFields[1];
for (int i = 2; i < infoFields.length; i++) {
String field = infoFields[i];
int idx = field.indexOf(':');
if (idx > 0) {
String key = field.substring(0, idx);
String value = field.substring(idx + 1);
if (!value.isEmpty()) {
propertiesMap.put(key, value);
}
}
}
model = propertiesMap.getOrDefault("model", serial);
} else {
LOG.error("Unable to extract device information from {}", LogUtils.escape(info));
serial = "";
state = "unknown";
model = "unknown";
}
adbHost = host;
adbPort = port;
}
public boolean isOnline() {
return state.equals("device");
}
public String getAdbHost() {
return adbHost;
}
public int getAdbPort() {
return adbPort;
}
public String getSerial() {
return serial;
}
public String getState() {
return state;
}
public String getModel() {
return model;
}
public String getAllInfo() {
return allInfo;
}
public String getProperty(String key) {
return this.propertiesMap.get(key);
}
@Override
public String toString() {
return allInfo;
}
static ADBDeviceInfo make(String info, String host, int port) {
ADBDeviceInfo deviceInfo = new ADBDeviceInfo();
String[] infoFields = info.trim().split("\\s+");
deviceInfo.allInfo = String.join(" ", infoFields);
if (infoFields.length > 2) {
deviceInfo.serial = infoFields[0];
deviceInfo.state = infoFields[1];
}
int pos = info.indexOf("model:");
if (pos != -1) {
int spacePos = info.indexOf(" ", pos);
if (spacePos != -1) {
deviceInfo.model = info.substring(pos + "model:".length(), spacePos);
}
}
if (deviceInfo.model == null || deviceInfo.model.equals("")) {
deviceInfo.model = deviceInfo.serial;
}
deviceInfo.adbHost = host;
deviceInfo.adbPort = port;
return deviceInfo;
}
}
@@ -145,8 +145,8 @@ public class BackgroundExecutor {
task.onDone(this);
// treat UI task operations as part of the task to not mix with others
UiUtils.uiRunAndWait(() -> {
task.onFinish(this);
progressPane.setVisible(false);
task.onFinish(this);
});
} finally {
taskComplete(id);
@@ -190,13 +190,21 @@ public class BackgroundExecutor {
performCancel(executor);
return cancelStatus;
}
updateProgress(executor);
k++;
Thread.sleep(k < 10 ? 200 : 1000); // faster update for short tasks
if (jobsCount == 1 && k == 3) {
if (k < 10) {
// faster update for short tasks
Thread.sleep(200);
if (k == 5) {
updateProgress(executor);
}
} else {
updateProgress(executor);
Thread.sleep(1000);
}
if (jobsCount == 1 && k == 5) {
// small delay before show progress to reduce blinking on short tasks
progressPane.changeVisibility(this, true);
}
k++;
}
} catch (InterruptedException e) {
LOG.debug("Task wait interrupted");
@@ -210,7 +218,7 @@ public class BackgroundExecutor {
}
private void updateProgress(ThreadPoolExecutor executor) {
Consumer<ITaskProgress> onProgressListener = task.getOnProgressListener();
Consumer<ITaskProgress> onProgressListener = task.getProgressListener();
ITaskProgress taskProgress = task.getTaskProgress();
if (taskProgress == null) {
setProgress(calcProgress(executor.getCompletedTaskCount(), jobsCount));
@@ -231,13 +239,16 @@ public class BackgroundExecutor {
// force termination
task.cancel();
executor.shutdown();
if (executor.awaitTermination(2, TimeUnit.SECONDS)) {
LOG.debug("Task cancel complete");
return;
int cancelTimeout = task.getCancelTimeoutMS();
if (cancelTimeout != 0) {
if (executor.awaitTermination(cancelTimeout, TimeUnit.MILLISECONDS)) {
LOG.debug("Task cancel complete");
return;
}
}
LOG.debug("Forcing tasks cancel");
executor.shutdownNow();
boolean complete = executor.awaitTermination(5, TimeUnit.SECONDS);
boolean complete = executor.awaitTermination(task.getShutdownTimeoutMS(), TimeUnit.MILLISECONDS);
LOG.debug("Forced task cancel status: {}",
complete ? "success" : "fail, still active: " + executor.getActiveCount());
}
@@ -301,5 +312,13 @@ public class BackgroundExecutor {
public long getTime() {
return time;
}
@Override
public String toString() {
return "TaskWorker{status=" + status
+ ", jobsCount=" + jobsCount
+ ", jobsComplete=" + jobsComplete
+ ", time=" + time + '}';
}
}
}
@@ -4,4 +4,12 @@ public interface Cancelable {
boolean isCanceled();
void cancel();
default int getCancelTimeoutMS() {
return 2000;
}
default int getShutdownTimeoutMS() {
return 5000;
}
}
@@ -54,7 +54,7 @@ public interface IBackgroundTask extends Cancelable {
/**
* Return progress notifications listener (use executor tick rate and thread) (Optional)
*/
default @Nullable Consumer<ITaskProgress> getOnProgressListener() {
default @Nullable Consumer<ITaskProgress> getProgressListener() {
return null;
}
}
@@ -107,7 +107,7 @@ public class QuarkManager {
private void loadReport() {
try {
QuarkReportNode quarkNode = new QuarkReportNode(reportFile);
JRoot root = mainWindow.getCacheObject().getJRoot();
JRoot root = mainWindow.getTreeRoot();
root.replaceCustomNode(quarkNode);
root.update();
mainWindow.reloadTree();
@@ -4,8 +4,10 @@ import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
import org.jetbrains.annotations.NotNull;
@@ -27,8 +29,8 @@ public class SearchTask extends CancelableBackgroundTask {
private static final Logger LOG = LoggerFactory.getLogger(SearchTask.class);
private final BackgroundExecutor backgroundExecutor;
private final Consumer<ITaskInfo> onFinish;
private final Consumer<JNode> results;
private final Consumer<JNode> resultsListener;
private final BiConsumer<ITaskInfo, Boolean> onFinish;
private final List<SearchJob> jobs = new ArrayList<>();
private final TaskProgress taskProgress = new TaskProgress();
@@ -39,9 +41,9 @@ public class SearchTask extends CancelableBackgroundTask {
private Consumer<ITaskProgress> progressListener;
public SearchTask(MainWindow mainWindow, Consumer<JNode> results, Consumer<ITaskInfo> onFinish) {
public SearchTask(MainWindow mainWindow, Consumer<JNode> results, BiConsumer<ITaskInfo, Boolean> onFinish) {
this.backgroundExecutor = mainWindow.getBackgroundExecutor();
this.results = results;
this.resultsListener = results;
this.onFinish = onFinish;
}
@@ -55,8 +57,7 @@ public class SearchTask extends CancelableBackgroundTask {
public synchronized void fetchResults() {
if (future != null) {
cancel();
waitTask();
throw new IllegalStateException("Previous task not yet finished");
}
resetCancel();
complete.set(false);
@@ -70,9 +71,10 @@ public class SearchTask extends CancelableBackgroundTask {
// ignore new results after cancel
return true;
}
this.results.accept(resultNode);
this.resultsListener.accept(resultNode);
if (resultsLimit != 0 && resultsCount.incrementAndGet() >= resultsLimit) {
cancel();
complete.set(false);
return true;
}
return false;
@@ -81,14 +83,17 @@ public class SearchTask extends CancelableBackgroundTask {
public synchronized void waitTask() {
if (future != null) {
try {
future.get(2, TimeUnit.SECONDS);
future.get(200, TimeUnit.MILLISECONDS);
} catch (TimeoutException e) {
LOG.debug("Search task wait timeout");
} catch (Exception e) {
LOG.warn("Wait search task failed", e);
LOG.warn("Search task wait error", e);
} finally {
future.cancel(true);
future = null;
}
}
}
@Override
@@ -101,18 +106,12 @@ public class SearchTask extends CancelableBackgroundTask {
return jobs;
}
public boolean isSearchComplete() {
return complete.get() && !isCanceled();
}
@Override
public void onDone(ITaskInfo taskInfo) {
this.complete.set(true);
}
@Override
public void onFinish(ITaskInfo status) {
this.onFinish.accept(status);
public void onFinish(ITaskInfo task) {
boolean complete = !isCanceled()
&& task.getStatus() == TaskStatus.COMPLETE
&& task.getJobsComplete() == task.getJobsCount();
this.onFinish.accept(task, complete);
}
@Override
@@ -131,7 +130,17 @@ public class SearchTask extends CancelableBackgroundTask {
}
@Override
public @Nullable Consumer<ITaskProgress> getOnProgressListener() {
public @Nullable Consumer<ITaskProgress> getProgressListener() {
return this.progressListener;
}
@Override
public int getCancelTimeoutMS() {
return 0;
}
@Override
public int getShutdownTimeoutMS() {
return 10;
}
}
@@ -1,13 +1,10 @@
package jadx.gui.search.providers;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.ArrayDeque;
import java.util.Deque;
import java.util.Enumeration;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
import javax.swing.tree.TreeNode;
@@ -18,56 +15,55 @@ import org.slf4j.LoggerFactory;
import jadx.api.ICodeWriter;
import jadx.api.ResourceFile;
import jadx.api.ResourceType;
import jadx.core.utils.files.FileUtils;
import jadx.api.plugins.utils.CommonFileUtils;
import jadx.gui.jobs.Cancelable;
import jadx.gui.search.ISearchProvider;
import jadx.gui.search.SearchSettings;
import jadx.gui.treemodel.JNode;
import jadx.gui.treemodel.JResSearchNode;
import jadx.gui.treemodel.JResource;
import jadx.gui.treemodel.JRoot;
import jadx.gui.ui.MainWindow;
import jadx.gui.utils.CacheObject;
public class ResourceSearchProvider implements ISearchProvider {
private static final Logger LOG = LoggerFactory.getLogger(ResourceSearchProvider.class);
private final CacheObject cache;
private final SearchSettings searchSettings;
private final Set<String> extSet = new HashSet<>();
private List<JResource> resNodes;
private String fileExts;
private final Set<String> extSet;
private final int sizeLimit;
private boolean anyExt;
private int sizeLimit;
private int progress;
/**
* Resources queue for process. Using UI nodes to reuse loading cache
*/
private final Deque<JResource> resQueue;
private int pos;
public ResourceSearchProvider(MainWindow mw, SearchSettings searchSettings) {
this.cache = mw.getCacheObject();
this.searchSettings = searchSettings;
this.sizeLimit = mw.getSettings().getSrhResourceSkipSize() * 1048576;
this.extSet = buildAllowedFilesExtensions(mw.getSettings().getSrhResourceFileExt());
this.resQueue = initResQueue(mw);
}
@Override
public @Nullable JNode next(Cancelable cancelable) {
if (resNodes == null) {
load();
}
if (resNodes.isEmpty()) {
return null;
}
while (true) {
if (cancelable.isCanceled()) {
return null;
}
JResource resNode = resNodes.get(progress);
JResource resNode = getNextResFile(cancelable);
if (resNode == null) {
return null;
}
JNode newResult = search(resNode);
if (newResult != null) {
return newResult;
}
progress++;
pos = 0;
if (progress >= resNodes.size()) {
resQueue.removeLast();
addChildren(resNode);
if (resQueue.isEmpty()) {
return null;
}
}
@@ -94,133 +90,107 @@ public class ResourceSearchProvider implements ISearchProvider {
return new JResSearchNode(resNode, line.trim(), newPos);
}
private synchronized void load() {
resNodes = new ArrayList<>();
sizeLimit = cache.getJadxSettings().getSrhResourceSkipSize() * 1048576;
fileExts = cache.getJadxSettings().getSrhResourceFileExt();
for (String extStr : fileExts.split("\\|")) {
private @Nullable JResource getNextResFile(Cancelable cancelable) {
while (true) {
JResource node = resQueue.peekLast();
if (node == null) {
return null;
}
try {
node.loadNode();
} catch (Exception e) {
LOG.error("Error load resource node: {}", node, e);
resQueue.removeLast();
continue;
}
if (cancelable.isCanceled()) {
return null;
}
if (node.getType() == JResource.JResType.FILE) {
if (shouldProcess(node)) {
return node;
}
resQueue.removeLast();
} else {
// dir
resQueue.removeLast();
addChildren(node);
}
}
}
private void addChildren(JResource resNode) {
resQueue.addAll(resNode.getSubNodes());
}
private static Deque<JResource> initResQueue(MainWindow mw) {
JRoot jRoot = mw.getTreeRoot();
Deque<JResource> deque = new ArrayDeque<>(jRoot.getChildCount());
Enumeration<TreeNode> children = jRoot.children();
while (children.hasMoreElements()) {
TreeNode node = children.nextElement();
if (node instanceof JResource) {
JResource resNode = (JResource) node;
deque.add(resNode);
}
}
return deque;
}
private Set<String> buildAllowedFilesExtensions(String srhResourceFileExt) {
Set<String> set = new HashSet<>();
for (String extStr : srhResourceFileExt.split("[|.]")) {
String ext = extStr.trim();
if (!ext.isEmpty()) {
anyExt = ext.equals("*");
if (anyExt) {
break;
}
extSet.add(ext);
set.add(ext);
}
}
try (ZipFile zipFile = getZipFile(cache.getJRoot())) {
traverseTree(cache.getJRoot(), zipFile); // reindex
} catch (Exception e) {
LOG.error("Failed to apply settings to resource index", e);
}
return set;
}
private void traverseTree(TreeNode root, @Nullable ZipFile zip) {
for (int i = 0; i < root.getChildCount(); i++) {
TreeNode node = root.getChildAt(i);
if (node instanceof JResource) {
JResource resNode = (JResource) node;
try {
resNode.loadNode();
} catch (Exception e) {
LOG.error("Error load resource node: {}", resNode, e);
return;
}
ResourceFile resFile = resNode.getResFile();
if (resFile == null) {
traverseTree(node, zip);
} else {
if (resFile.getType() == ResourceType.ARSC && shouldSearchXML()) {
resFile.loadContent();
resNode.getFiles().forEach(t -> traverseTree(t, null));
} else {
filter(resNode, zip);
}
}
}
}
}
private boolean shouldSearchXML() {
return anyExt || fileExts.contains(".xml");
}
@Nullable
private ZipFile getZipFile(TreeNode res) {
for (int i = 0; i < res.getChildCount(); i++) {
TreeNode node = res.getChildAt(i);
if (node instanceof JResource) {
JResource resNode = (JResource) node;
try {
resNode.loadNode();
} catch (Exception e) {
LOG.error("Error load resource node: {}", resNode, e);
return null;
}
ResourceFile file = resNode.getResFile();
if (file == null) {
ZipFile zip = getZipFile(resNode);
if (zip != null) {
return zip;
}
} else {
ResourceFile.ZipRef zipRef = file.getZipRef();
if (zipRef != null) {
File zfile = zipRef.getZipFile();
if (FileUtils.isZipFile(zfile)) {
try {
return new ZipFile(zfile);
} catch (IOException ignore) {
}
}
}
}
}
}
return null;
}
private void filter(JResource resNode, ZipFile zip) {
private boolean shouldProcess(JResource resNode) {
ResourceFile resFile = resNode.getResFile();
if (JResource.isSupportedForView(resFile.getType())) {
long size = -1;
if (zip != null) {
ZipEntry entry = zip.getEntry(resFile.getOriginalName());
if (entry != null) {
size = entry.getSize();
}
if (resFile.getType() == ResourceType.ARSC) {
// don't check size of generated resource table, it will also skip all sub files
return anyExt || extSet.contains("xml");
}
if (!anyExt) {
String fileExt = CommonFileUtils.getFileExtension(resFile.getOriginalName());
if (fileExt == null) {
return false;
}
if (size == -1) { // resource from ARSC is unknown size
try {
size = resNode.getCodeInfo().getCodeStr().length();
} catch (Exception ignore) {
return;
}
if (!extSet.contains(fileExt)) {
return false;
}
if (size <= sizeLimit) {
if (!anyExt) {
for (String ext : extSet) {
if (resFile.getOriginalName().endsWith(ext)) {
resNodes.add(resNode);
break;
}
}
} else {
resNodes.add(resNode);
}
} else {
LOG.debug("Resource index skipped because of size limit: {} res size {} bytes", resNode, size);
}
if (sizeLimit == 0) {
return true;
}
try {
int charsCount = resNode.getCodeInfo().getCodeStr().length();
long size = charsCount * 8L;
if (size > sizeLimit) {
LOG.debug("Resource search skipped because of size limit: {} res size {} bytes", resNode, size);
return false;
}
return true;
} catch (Exception e) {
LOG.warn("Resource load error: {}", resNode, e);
return false;
}
}
@Override
public int progress() {
return progress;
return 0;
}
@Override
public int total() {
return resNodes == null ? 0 : resNodes.size();
return 0;
}
}
@@ -43,6 +43,8 @@ public class JadxProject {
private static final int CURRENT_PROJECT_VERSION = 1;
public static final String PROJECT_EXTENSION = "jadx";
private static final int SEARCH_HISTORY_LIMIT = 30;
private final transient MainWindow mainWindow;
private transient String name = "New Project";
@@ -135,6 +137,9 @@ public class JadxProject {
.map(TabStateViewAdapter::build)
.filter(Objects::nonNull)
.collect(Collectors.toList());
if (tabStateList.isEmpty()) {
return;
}
boolean dataChanged;
dataChanged = data.setOpenTabs(tabStateList);
dataChanged |= data.setActiveTab(activeTab);
@@ -192,6 +197,27 @@ public class JadxProject {
}
}
public List<String> getSearchHistory() {
return data.getSearchHistory();
}
public void addToSearchHistory(String str) {
if (str == null || str.isEmpty()) {
return;
}
List<String> list = data.getSearchHistory();
if (!list.isEmpty() && list.get(0).equals(str)) {
return;
}
list.remove(str);
list.add(0, str);
if (list.size() > SEARCH_HISTORY_LIMIT) {
list.remove(list.size() - 1);
}
data.setSearchHistory(list);
changed();
}
private void changed() {
JadxSettings settings = mainWindow.getSettings();
if (settings != null && settings.isAutoSaveProject()) {
@@ -30,6 +30,7 @@ import jadx.api.CommentsLevel;
import jadx.api.DecompilationMode;
import jadx.api.JadxArgs;
import jadx.api.args.DeobfuscationMapFileMode;
import jadx.api.args.ResourceNameSource;
import jadx.cli.JadxCLIArgs;
import jadx.cli.LogHelper;
import jadx.gui.ui.MainWindow;
@@ -78,6 +79,7 @@ public class JadxSettings extends JadxCLIArgs {
private boolean codeAreaLineWrap = false;
private int srhResourceSkipSize = 1000;
private String srhResourceFileExt = ".xml|.html|.js|.json|.txt";
private boolean useAutoSearch = true;
private boolean keepCommonDialogOpen = false;
private boolean smaliAreaShowBytecode = false;
private LineNumbersMode lineNumbersMode = LineNumbersMode.AUTO;
@@ -209,6 +211,11 @@ public class JadxSettings extends JadxCLIArgs {
partialSync(settings -> settings.recentProjects = recentProjects);
}
public void removeRecentProject(Path projectPath) {
recentProjects.remove(projectPath);
partialSync(settings -> settings.recentProjects = recentProjects);
}
public void saveWindowPos(Window window) {
WindowLocation pos = new WindowLocation(window.getClass().getSimpleName(), window.getBounds());
windowPos.put(pos.getWindowId(), pos);
@@ -348,6 +355,10 @@ public class JadxSettings extends JadxCLIArgs {
this.useKotlinMethodsForVarNames = useKotlinMethodsForVarNames;
}
public void setResourceNameSource(ResourceNameSource source) {
this.resourceNameSource = source;
}
public void updateRenameFlag(JadxArgs.RenameEnum flag, boolean enabled) {
if (enabled) {
renameFlags.add(flag);
@@ -380,6 +391,10 @@ public class JadxSettings extends JadxCLIArgs {
this.inlineMethods = inlineMethods;
}
public void setExtractFinally(boolean extractFinally) {
this.extractFinally = extractFinally;
}
public void setFsCaseSensitive(boolean fsCaseSensitive) {
this.fsCaseSensitive = fsCaseSensitive;
}
@@ -526,6 +541,15 @@ public class JadxSettings extends JadxCLIArgs {
srhResourceFileExt = all.trim();
}
public boolean isUseAutoSearch() {
return useAutoSearch;
}
public void setUseAutoSearch(boolean useAutoSearch) {
this.useAutoSearch = useAutoSearch;
partialSync(settings -> settings.useAutoSearch = useAutoSearch);
}
public void setKeepCommonDialogOpen(boolean yes) {
keepCommonDialogOpen = yes;
}
@@ -63,6 +63,7 @@ import jadx.api.DecompilationMode;
import jadx.api.JadxArgs;
import jadx.api.JadxArgs.UseKotlinMethodsForVarNames;
import jadx.api.args.DeobfuscationMapFileMode;
import jadx.api.args.ResourceNameSource;
import jadx.api.plugins.JadxPlugin;
import jadx.api.plugins.JadxPluginInfo;
import jadx.api.plugins.options.JadxPluginOptions;
@@ -271,6 +272,13 @@ public class JadxSettingsWindow extends JDialog {
needReload();
});
JComboBox<ResourceNameSource> resNamesSource = new JComboBox<>(ResourceNameSource.values());
resNamesSource.setSelectedItem(settings.getResourceNameSource());
resNamesSource.addActionListener(e -> {
settings.setResourceNameSource((ResourceNameSource) resNamesSource.getSelectedItem());
needReload();
});
JComboBox<DeobfuscationMapFileMode> deobfMapFileModeCB = new JComboBox<>(DeobfuscationMapFileMode.values());
deobfMapFileModeCB.setSelectedItem(settings.getDeobfuscationMapFileMode());
deobfMapFileModeCB.addActionListener(e -> {
@@ -287,6 +295,7 @@ public class JadxSettingsWindow extends JDialog {
deobfGroup.addRow(NLS.str("preferences.deobfuscation_max_len"), maxLenSpinner);
deobfGroup.addRow(NLS.str("preferences.deobfuscation_source_alias"), deobfSourceAlias);
deobfGroup.addRow(NLS.str("preferences.deobfuscation_kotlin_metadata"), deobfKotlinMetadata);
deobfGroup.addRow(NLS.str("preferences.deobfuscation_res_name_source"), resNamesSource);
deobfGroup.addRow(NLS.str("preferences.deobfuscation_map_file_mode"), deobfMapFileModeCB);
deobfGroup.end();
@@ -533,6 +542,13 @@ public class JadxSettingsWindow extends JDialog {
needReload();
});
JCheckBox extractFinally = new JCheckBox();
extractFinally.setSelected(settings.isExtractFinally());
extractFinally.addItemListener(e -> {
settings.setExtractFinally(e.getStateChange() == ItemEvent.SELECTED);
needReload();
});
JCheckBox fsCaseSensitive = new JCheckBox();
fsCaseSensitive.setSelected(settings.isFsCaseSensitive());
fsCaseSensitive.addItemListener(e -> {
@@ -569,6 +585,7 @@ public class JadxSettingsWindow extends JDialog {
other.addRow(NLS.str("preferences.useDebugInfo"), useDebugInfo);
other.addRow(NLS.str("preferences.inlineAnonymous"), inlineAnonymous);
other.addRow(NLS.str("preferences.inlineMethods"), inlineMethods);
other.addRow(NLS.str("preferences.extractFinally"), extractFinally);
other.addRow(NLS.str("preferences.fsCaseSensitive"), fsCaseSensitive);
other.addRow(NLS.str("preferences.useDx"), useDx);
other.addRow(NLS.str("preferences.skipResourcesDecode"), resourceDecode);
@@ -20,6 +20,7 @@ public class ProjectData {
private int activeTab = -1;
private @Nullable Path cacheDir;
private boolean enableLiveReload = false;
private List<String> searchHistory = new ArrayList<>();
public List<Path> getFiles() {
return files;
@@ -103,4 +104,12 @@ public class ProjectData {
public void setEnableLiveReload(boolean enableLiveReload) {
this.enableLiveReload = enableLiveReload;
}
public List<String> getSearchHistory() {
return searchHistory;
}
public void setSearchHistory(List<String> searchHistory) {
this.searchHistory = searchHistory;
}
}
@@ -14,6 +14,7 @@ import jadx.api.JavaNode;
import jadx.core.dex.attributes.AFlag;
import jadx.core.dex.info.AccessInfo;
import jadx.core.dex.instructions.args.ArgType;
import jadx.gui.utils.Icons;
import jadx.gui.utils.OverlayIcon;
import jadx.gui.utils.UiUtils;
@@ -84,10 +85,10 @@ public class JMethod extends JNode {
OverlayIcon overIcon = new OverlayIcon(icon);
if (accessFlags.isFinal()) {
overIcon.add(UiUtils.ICON_FINAL);
overIcon.add(Icons.FINAL);
}
if (accessFlags.isStatic()) {
overIcon.add(UiUtils.ICON_STATIC);
overIcon.add(Icons.STATIC);
}
return overIcon;
@@ -1,6 +1,6 @@
package jadx.gui.treemodel;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
@@ -14,10 +14,10 @@ import org.jetbrains.annotations.Nullable;
import jadx.api.ICodeInfo;
import jadx.api.ICodeWriter;
import jadx.api.ResourceFile;
import jadx.api.ResourceFileContent;
import jadx.api.ResourceType;
import jadx.api.ResourcesLoader;
import jadx.api.impl.SimpleCodeInfo;
import jadx.core.utils.ListUtils;
import jadx.core.utils.Utils;
import jadx.core.xmlgen.ResContainer;
import jadx.gui.ui.TabbedPane;
@@ -26,6 +26,7 @@ import jadx.gui.ui.panel.ContentPanel;
import jadx.gui.ui.panel.ImagePanel;
import jadx.gui.utils.NLS;
import jadx.gui.utils.UiUtils;
import jadx.gui.utils.res.ResTableHelper;
public class JResource extends JLoadableNode {
private static final long serialVersionUID = -201018424302612434L;
@@ -41,6 +42,10 @@ public class JResource extends JLoadableNode {
private static final ImageIcon JAVA_ICON = UiUtils.openSvgIcon("nodes/java");
private static final ImageIcon UNKNOWN_ICON = UiUtils.openSvgIcon("nodes/unknown");
public static final Comparator<JResource> RESOURCES_COMPARATOR =
Comparator.<JResource>comparingInt(r -> r.type.ordinal())
.thenComparing(JResource::getName, String.CASE_INSENSITIVE_ORDER);
public enum JResType {
ROOT,
DIR,
@@ -49,11 +54,11 @@ public class JResource extends JLoadableNode {
private final transient String name;
private final transient String shortName;
private final transient List<JResource> files = new ArrayList<>(1);
private final transient JResType type;
private final transient ResourceFile resFile;
private transient boolean loaded;
private transient volatile boolean loaded;
private transient List<JResource> subNodes = Collections.emptyList();
private transient ICodeInfo content;
public JResource(ResourceFile resFile, String name, JResType type) {
@@ -69,7 +74,8 @@ public class JResource extends JLoadableNode {
}
public final void update() {
if (files.isEmpty()) {
removeAllChildren();
if (Utils.isEmpty(subNodes)) {
if (type == JResType.DIR || type == JResType.ROOT
|| resFile.getType() == ResourceType.ARSC) {
// fake leaf to force show expand button
@@ -77,14 +83,7 @@ public class JResource extends JLoadableNode {
add(new TextNode(NLS.str("tree.loading")));
}
} else {
removeAllChildren();
Comparator<JResource> typeComparator = Comparator.comparingInt(r -> r.type.ordinal());
Comparator<JResource> nameComparator = Comparator.comparing(JResource::getName, String.CASE_INSENSITIVE_ORDER);
files.sort(typeComparator.thenComparing(nameComparator));
for (JResource res : files) {
for (JResource res : subNodes) {
res.update();
add(res);
}
@@ -92,7 +91,7 @@ public class JResource extends JLoadableNode {
}
@Override
public void loadNode() {
public synchronized void loadNode() {
getCodeInfo();
update();
}
@@ -102,8 +101,27 @@ public class JResource extends JLoadableNode {
return name;
}
public List<JResource> getFiles() {
return files;
public JResType getType() {
return type;
}
public List<JResource> getSubNodes() {
return subNodes;
}
public void addSubNode(JResource node) {
subNodes = ListUtils.safeAdd(subNodes, node);
}
public void sortSubNodes() {
sortResNodes(subNodes);
}
private static void sortResNodes(List<JResource> nodes) {
if (Utils.notEmpty(nodes)) {
nodes.forEach(JResource::sortSubNodes);
nodes.sort(RESOURCES_COMPARATOR);
}
}
@Override
@@ -141,9 +159,9 @@ public class JResource extends JLoadableNode {
}
if (rc.getDataType() == ResContainer.DataType.RES_TABLE) {
ICodeInfo codeInfo = loadCurrentSingleRes(rc);
for (ResContainer subFile : rc.getSubFiles()) {
loadSubNodes(this, subFile, 1);
}
List<JResource> nodes = ResTableHelper.buildTree(rc);
sortResNodes(nodes);
subNodes = nodes;
return codeInfo;
}
// single node
@@ -174,47 +192,6 @@ public class JResource extends JLoadableNode {
}
}
private void loadSubNodes(JResource root, ResContainer rc, int depth) {
String resName = rc.getName();
String[] path = resName.split("/");
String resShortName = path.length == 0 ? resName : path[path.length - 1];
ICodeInfo code = rc.getText();
ResourceFileContent fileContent = new ResourceFileContent(resShortName, ResourceType.XML, code);
addPath(path, root, new JResource(fileContent, resName, resShortName, JResType.FILE));
for (ResContainer subFile : rc.getSubFiles()) {
loadSubNodes(root, subFile, depth + 1);
}
}
private static void addPath(String[] path, JResource root, JResource jResource) {
if (path.length == 1) {
root.getFiles().add(jResource);
return;
}
JResource currentRoot = root;
int last = path.length - 1;
for (int i = 0; i <= last; i++) {
String f = path[i];
if (i == last) {
currentRoot.getFiles().add(jResource);
} else {
currentRoot = getResDir(currentRoot, f);
}
}
}
private static JResource getResDir(JResource root, String dirName) {
for (JResource file : root.getFiles()) {
if (file.getName().equals(dirName)) {
return file;
}
}
JResource resDir = new JResource(null, dirName, JResType.DIR);
root.getFiles().add(resDir);
return resDir;
}
@Override
public String getSyntaxName() {
if (resFile == null) {
@@ -66,19 +66,20 @@ public class JRoot extends JNode {
if (i != count - 1) {
subRF = new JResource(null, name, JResType.DIR);
} else {
subRF = new JResource(rf, rf.getOriginalName(), name, JResType.FILE);
subRF = new JResource(rf, rf.getDeobfName(), name, JResType.FILE);
}
curRf.getFiles().add(subRF);
curRf.addSubNode(subRF);
}
curRf = subRF;
}
}
root.sortSubNodes();
root.update();
return root;
}
private JResource getResourceByName(JResource rf, String name) {
for (JResource sub : rf.getFiles()) {
for (JResource sub : rf.getSubNodes()) {
if (sub.getName().equals(name)) {
return sub;
}
@@ -125,12 +125,15 @@ import jadx.gui.ui.panel.IssuesPanel;
import jadx.gui.ui.panel.JDebuggerPanel;
import jadx.gui.ui.panel.ProgressPanel;
import jadx.gui.ui.popupmenu.JPackagePopupMenu;
import jadx.gui.ui.treenodes.StartPageNode;
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.CacheObject;
import jadx.gui.utils.FontUtils;
import jadx.gui.utils.ILoadListener;
import jadx.gui.utils.Icons;
import jadx.gui.utils.LafManager;
import jadx.gui.utils.Link;
import jadx.gui.utils.NLS;
@@ -152,7 +155,6 @@ public class MainWindow extends JFrame {
private static final double WINDOW_RATIO = 1 - BORDER_RATIO * 2;
public static final double SPLIT_PANE_RESIZE_WEIGHT = 0.15;
private static final ImageIcon ICON_OPEN = UiUtils.openSvgIcon("ui/openDisk");
private static final ImageIcon ICON_ADD_FILES = UiUtils.openSvgIcon("ui/addFile");
private static final ImageIcon ICON_SAVE_ALL = UiUtils.openSvgIcon("ui/menu-saveall");
private static final ImageIcon ICON_RELOAD = UiUtils.openSvgIcon("ui/refresh");
@@ -210,6 +212,9 @@ public class MainWindow extends JFrame {
private JDebuggerPanel debuggerPanel;
private JSplitPane verticalSplitter;
private List<ILoadListener> loadListeners = new ArrayList<>();
private boolean loaded;
public MainWindow(JadxSettings settings) {
this.settings = settings;
this.cacheObject = new CacheObject();
@@ -249,7 +254,7 @@ public class MainWindow extends JFrame {
private void processCommandLineArgs() {
if (settings.getFiles().isEmpty()) {
openFileOrProject();
tabbedPane.showNode(new StartPageNode());
} else {
open(FileUtils.fileNamesToPaths(settings.getFiles()), this::handleSelectClassOption);
}
@@ -286,12 +291,20 @@ public class MainWindow extends JFrame {
});
}
public void openFileOrProject() {
public void openFileDialog() {
showOpenDialog(FileDialog.OpenMode.OPEN);
}
public void openProjectDialog() {
showOpenDialog(FileDialog.OpenMode.OPEN_PROJECT);
}
private void showOpenDialog(FileDialog.OpenMode mode) {
saveAll();
if (!ensureProjectIsSaved()) {
return;
}
FileDialog fileDialog = new FileDialog(this, FileDialog.OpenMode.OPEN);
FileDialog fileDialog = new FileDialog(this, mode);
List<Path> openPaths = fileDialog.show();
if (!openPaths.isEmpty()) {
settings.setLastOpenFilePath(fileDialog.getCurrentDir());
@@ -313,6 +326,7 @@ public class MainWindow extends JFrame {
}
private void newProject() {
saveAll();
if (!ensureProjectIsSaved()) {
return;
}
@@ -386,7 +400,11 @@ public class MainWindow extends JFrame {
s -> update());
}
void open(List<Path> paths) {
public void open(Path path) {
open(Collections.singletonList(path), EMPTY_RUNNABLE);
}
public void open(List<Path> paths) {
open(paths, EMPTY_RUNNABLE);
}
@@ -411,7 +429,7 @@ public class MainWindow extends JFrame {
// check if project file already saved with default name
Path projectPath = getProjectPathForFile(singleFile);
if (Files.exists(projectPath)) {
LOG.info("Loading project for this file");
LOG.info("Loading project {}", projectPath);
openProject(projectPath, onFinish);
return true;
}
@@ -474,6 +492,7 @@ public class MainWindow extends JFrame {
}
private void closeAll() {
notifyLoadListeners(false);
cancelBackgroundJobs();
clearTree();
resetCache();
@@ -512,7 +531,10 @@ public class MainWindow extends JFrame {
backgroundExecutor.execute(NLS.str("progress.load"),
this::restoreOpenTabs,
status -> runInitialBackgroundJobs());
status -> {
runInitialBackgroundJobs();
notifyLoadListeners(true);
});
}
public void updateLiveReload(boolean state) {
@@ -574,8 +596,6 @@ public class MainWindow extends JFrame {
protected void resetCache() {
cacheObject.reset();
cacheObject.setJRoot(treeRoot);
cacheObject.setJadxSettings(settings);
}
synchronized void runInitialBackgroundJobs() {
@@ -658,13 +678,11 @@ public class MainWindow extends JFrame {
public void initTree() {
treeRoot = new JRoot(wrapper);
cacheObject.setJRoot(treeRoot);
treeRoot.setFlatPackages(isFlattenPackage);
treeModel.setRoot(treeRoot);
addTreeCustomNodes();
treeRoot.update();
reloadTree();
cacheObject.setJadxSettings(settings);
}
private void clearTree() {
@@ -825,14 +843,15 @@ public class MainWindow extends JFrame {
}
private void initMenuAndToolbar() {
Action openAction = new AbstractAction(NLS.str("file.open_action"), ICON_OPEN) {
@Override
public void actionPerformed(ActionEvent e) {
openFileOrProject();
}
};
openAction.putValue(Action.SHORT_DESCRIPTION, NLS.str("file.open_action"));
openAction.putValue(Action.ACCELERATOR_KEY, getKeyStroke(KeyEvent.VK_O, UiUtils.ctrlButton()));
ActionHandler openAction = new ActionHandler(this::openFileDialog);
openAction.setNameAndDesc(NLS.str("file.open_action"));
openAction.setIcon(Icons.OPEN);
openAction.setKeyBinding(getKeyStroke(KeyEvent.VK_O, UiUtils.ctrlButton()));
ActionHandler openProject = new ActionHandler(this::openProjectDialog);
openProject.setNameAndDesc(NLS.str("file.open_project"));
openProject.setIcon(Icons.OPEN_PROJECT);
openProject.setKeyBinding(getKeyStroke(KeyEvent.VK_O, InputEvent.SHIFT_DOWN_MASK | UiUtils.ctrlButton()));
Action addFilesAction = new AbstractAction(NLS.str("file.add_files_action"), ICON_ADD_FILES) {
@Override
@@ -842,7 +861,7 @@ public class MainWindow extends JFrame {
};
addFilesAction.putValue(Action.SHORT_DESCRIPTION, NLS.str("file.add_files_action"));
newProjectAction = new AbstractAction(NLS.str("file.new_project")) {
newProjectAction = new AbstractAction(NLS.str("file.new_project"), Icons.NEW_PROJECT) {
@Override
public void actionPerformed(ActionEvent e) {
newProject();
@@ -1089,6 +1108,7 @@ public class MainWindow extends JFrame {
JMenu file = new JMenu(NLS.str("menu.file"));
file.setMnemonic(KeyEvent.VK_F);
file.add(openAction);
file.add(openProject);
file.add(addFilesAction);
file.addSeparator();
file.add(newProjectAction);
@@ -1194,6 +1214,22 @@ public class MainWindow extends JFrame {
toolbar.add(updateLink);
mainPanel.add(toolbar, BorderLayout.NORTH);
addLoadListener(loaded -> {
textSearchAction.setEnabled(loaded);
clsSearchAction.setEnabled(loaded);
commentSearchAction.setEnabled(loaded);
backAction.setEnabled(loaded);
forwardAction.setEnabled(loaded);
syncAction.setEnabled(loaded);
saveAllAction.setEnabled(loaded);
exportAction.setEnabled(loaded);
saveProjectAsAction.setEnabled(loaded);
reload.setEnabled(loaded);
deobfAction.setEnabled(loaded);
quarkAction.setEnabled(loaded);
return false;
});
}
private void initUI() {
@@ -1483,6 +1519,17 @@ public class MainWindow extends JFrame {
settings.setDebuggerVarTreeSplitterLoc(debuggerPanel.getRightSplitterLocation());
}
public void addLoadListener(ILoadListener loadListener) {
this.loadListeners.add(loadListener);
// set initial value
loadListener.update(loaded);
}
public void notifyLoadListeners(boolean loaded) {
this.loaded = loaded;
loadListeners.removeIf(listener -> listener.update(loaded));
}
public JadxWrapper getWrapper() {
return wrapper;
}
@@ -9,7 +9,6 @@ import java.util.List;
import java.util.Map;
import javax.swing.BorderFactory;
import javax.swing.ImageIcon;
import javax.swing.JButton;
import javax.swing.JLabel;
import javax.swing.JMenuItem;
@@ -21,15 +20,13 @@ import javax.swing.plaf.basic.BasicButtonUI;
import jadx.gui.treemodel.JClass;
import jadx.gui.treemodel.JNode;
import jadx.gui.ui.panel.ContentPanel;
import jadx.gui.utils.Icons;
import jadx.gui.utils.NLS;
import jadx.gui.utils.UiUtils;
public class TabComponent extends JPanel {
private static final long serialVersionUID = -8147035487543610321L;
private static final ImageIcon ICON_CLOSE = UiUtils.openSvgIcon("ui/closeHovered");
private static final ImageIcon ICON_CLOSE_INACTIVE = UiUtils.openSvgIcon("ui/close");
private final TabbedPane tabbedPane;
private final ContentPanel contentPanel;
@@ -71,8 +68,8 @@ public class TabComponent extends JPanel {
label.setIcon(node.getIcon());
final JButton closeBtn = new JButton();
closeBtn.setIcon(ICON_CLOSE_INACTIVE);
closeBtn.setRolloverIcon(ICON_CLOSE);
closeBtn.setIcon(Icons.CLOSE_INACTIVE);
closeBtn.setRolloverIcon(Icons.CLOSE);
closeBtn.setRolloverEnabled(true);
closeBtn.setOpaque(false);
closeBtn.setUI(new BasicButtonUI());
@@ -3,6 +3,7 @@ package jadx.gui.ui.codearea;
import java.awt.event.KeyEvent;
import jadx.gui.treemodel.JNode;
import jadx.gui.ui.MainWindow;
import jadx.gui.ui.dialog.UsageDialog;
import jadx.gui.utils.NLS;
@@ -18,7 +19,15 @@ public final class FindUsageAction extends JNodeAction {
@Override
public void runAction(JNode node) {
UsageDialog usageDialog = new UsageDialog(getCodeArea().getMainWindow(), node);
MainWindow mw = getCodeArea().getMainWindow();
UsageDialog usageDialog = new UsageDialog(mw, node);
mw.addLoadListener(loaded -> {
if (!loaded) {
usageDialog.dispose();
return true;
}
return false;
});
usageDialog.setVisible(true);
}
}
@@ -79,8 +79,11 @@ public final class FridaAction extends JNodeAction {
JavaMethod javaMethod = jMth.getJavaMethod();
MethodInfo methodInfo = javaMethod.getMethodNode().getMethodInfo();
String methodName = StringEscapeUtils.escapeEcmaScript(methodInfo.getName());
String callMethodName = methodName;
if (methodInfo.isConstructor()) {
methodName = "$init";
callMethodName = "$new";
}
String shortClassName = javaMethod.getDeclaringClass().getName();
@@ -108,7 +111,7 @@ public final class FridaAction extends JNodeAction {
+ " console.log('%s ret value is ' + ret);\n"
+ " return ret;\n"
+ "};",
functionUntilImplementation, functionParametersString, methodName, logParametersString, methodName,
functionUntilImplementation, functionParametersString, methodName, logParametersString, callMethodName,
functionParametersString, methodName);
return generateClassSnippet(jMth.getJParent()) + "\n" + functionParameterAndBody;
@@ -359,7 +359,7 @@ public class ADBDialog extends JDialog implements ADB.DeviceStateListener, ADB.J
try {
return mainWindow.getDebuggerPanel().showDebugger(
debugSetter.name,
debugSetter.device.getDeviceInfo().adbHost,
debugSetter.device.getDeviceInfo().getAdbHost(),
debugSetter.forwardTcpPort,
debugSetter.ver);
} catch (Exception e) {
@@ -569,10 +569,10 @@ public class ADBDialog extends JDialog implements ADB.DeviceStateListener, ADB.J
void refresh() {
ADBDeviceInfo info = device.getDeviceInfo();
String text = info.model;
String text = info.getModel();
if (text != null) {
if (!text.equals(info.serial)) {
text += String.format(" [serial: %s]", info.serial);
if (!text.equals(info.getSerial())) {
text += String.format(" [serial: %s]", info.getSerial());
}
text += String.format(" [state: %s]", info.isOnline() ? "online" : "offline");
tNode.setUserObject(text);
@@ -668,8 +668,8 @@ public class ADBDialog extends JDialog implements ADB.DeviceStateListener, ADB.J
String jdwpPid = " jdwp:" + pid;
String tcpPort = " tcp:" + forwardTcpPort;
try {
List<String> list = ADB.listForward(device.getDeviceInfo().adbHost,
device.getDeviceInfo().adbPort);
List<String> list = ADB.listForward(device.getDeviceInfo().getAdbHost(),
device.getDeviceInfo().getAdbPort());
for (String s : list) {
if (s.startsWith(device.getSerial()) && s.endsWith(jdwpPid) && !s.contains(tcpPort)) {
String[] fields = s.split("\\s+");
@@ -693,8 +693,8 @@ public class ADBDialog extends JDialog implements ADB.DeviceStateListener, ADB.J
String jdwpPid = " jdwp:" + pid;
String tcpPort = " tcp:" + forwardTcpPort;
try {
List<String> list = ADB.listForward(device.getDeviceInfo().adbHost,
device.getDeviceInfo().adbPort);
List<String> list = ADB.listForward(device.getDeviceInfo().getAdbHost(),
device.getDeviceInfo().getAdbPort());
for (String s : list) {
if (s.startsWith(device.getSerial()) && s.endsWith(jdwpPid)) {
return !s.contains(tcpPort);
@@ -16,9 +16,7 @@ import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.swing.AbstractAction;
import javax.swing.BorderFactory;
@@ -77,9 +75,7 @@ public abstract class CommonSearchDialog extends JFrame {
protected JLabel warnLabel;
protected ProgressPanel progressPane;
private String highlightText;
protected boolean highlightTextCaseInsensitive = false;
protected boolean highlightTextUseRegex = false;
private SearchContext highlightContext;
public CommonSearchDialog(MainWindow mainWindow, String title) {
this.mainWindow = mainWindow;
@@ -88,7 +84,7 @@ public abstract class CommonSearchDialog extends JFrame {
this.codeFont = mainWindow.getSettings().getFont();
this.windowTitle = title;
UiUtils.setWindowIcons(this);
updateTitle();
updateTitle("");
}
protected abstract void openInit();
@@ -103,17 +99,24 @@ public abstract class CommonSearchDialog extends JFrame {
}
}
private void updateTitle() {
if (highlightText == null || highlightText.trim().isEmpty()) {
private void updateTitle(String searchText) {
if (searchText == null || searchText.isEmpty() || searchText.trim().isEmpty()) {
setTitle(windowTitle);
} else {
setTitle(windowTitle + ": " + highlightText);
setTitle(windowTitle + ": " + searchText);
}
}
public void setHighlightText(String highlightText) {
this.highlightText = highlightText;
updateTitle();
public void updateHighlightContext(String text, boolean caseSensitive, boolean regexp) {
updateTitle(text);
highlightContext = new SearchContext(text);
highlightContext.setMatchCase(caseSensitive);
highlightContext.setRegularExpression(regexp);
highlightContext.setMarkAll(true);
}
public void disableHighlight() {
highlightContext = null;
}
protected void registerInitOnOpen() {
@@ -147,11 +150,16 @@ public abstract class CommonSearchDialog extends JFrame {
@Nullable
private JNode getSelectedNode() {
int selectedId = resultsTable.getSelectedRow();
if (selectedId == -1) {
try {
int selectedId = resultsTable.getSelectedRow();
if (selectedId == -1 || selectedId >= resultsTable.getRowCount()) {
return null;
}
return (JNode) resultsModel.getValueAt(selectedId, 0);
} catch (Exception e) {
LOG.error("Failed to get results table selected object", e);
return null;
}
return (JNode) resultsModel.getValueAt(selectedId, 0);
}
@Override
@@ -174,16 +182,16 @@ public abstract class CommonSearchDialog extends JFrame {
openBtn.addActionListener(event -> openSelectedItem());
getRootPane().setDefaultButton(openBtn);
JPanel buttonPane = new JPanel();
buttonPane.setLayout(new BoxLayout(buttonPane, BoxLayout.LINE_AXIS));
buttonPane.setBorder(BorderFactory.createEmptyBorder(0, 10, 10, 10));
JCheckBox cbKeepOpen = new JCheckBox(NLS.str("search_dialog.keep_open"));
cbKeepOpen.setSelected(mainWindow.getSettings().getKeepCommonDialogOpen());
cbKeepOpen.addActionListener(e -> {
mainWindow.getSettings().setKeepCommonDialogOpen(cbKeepOpen.isSelected());
mainWindow.getSettings().sync();
});
cbKeepOpen.setAlignmentY(Component.CENTER_ALIGNMENT);
JPanel buttonPane = new JPanel();
buttonPane.setLayout(new BoxLayout(buttonPane, BoxLayout.LINE_AXIS));
buttonPane.add(cbKeepOpen);
buttonPane.add(Box.createRigidArea(new Dimension(15, 0)));
buttonPane.add(progressPane);
@@ -197,7 +205,7 @@ public abstract class CommonSearchDialog extends JFrame {
protected JPanel initResultsTable() {
ResultsTableCellRenderer renderer = new ResultsTableCellRenderer();
resultsModel = new ResultsModel(renderer);
resultsModel = new ResultsModel();
resultsModel.addTableModelListener(e -> updateProgressLabel(false));
resultsTable = new ResultsTable(resultsModel, renderer);
@@ -247,28 +255,27 @@ public abstract class CommonSearchDialog extends JFrame {
warnLabel.setVisible(false);
JScrollPane scroll = new JScrollPane(resultsTable, VERTICAL_SCROLLBAR_AS_NEEDED, HORIZONTAL_SCROLLBAR_AS_NEEDED);
// scroll.setBorder(BorderFactory.createEmptyBorder(0, 5, 0, 0));
resultsInfoLabel = new JLabel("");
resultsInfoLabel.setFont(mainWindow.getSettings().getFont());
JPanel resultsActionsPanel = new JPanel();
resultsActionsPanel.setLayout(new BoxLayout(resultsActionsPanel, BoxLayout.LINE_AXIS));
resultsActionsPanel.setBorder(BorderFactory.createEmptyBorder(10, 0, 10, 0));
addCustomResultsActions(resultsActionsPanel);
resultsInfoLabel = new JLabel("");
resultsInfoLabel.setFont(mainWindow.getSettings().getFont());
resultsActionsPanel.add(Box.createRigidArea(new Dimension(20, 0)));
resultsActionsPanel.add(resultsInfoLabel);
resultsActionsPanel.add(Box.createHorizontalGlue());
addResultsActions(resultsActionsPanel);
JPanel resultsPanel = new JPanel();
resultsPanel.setLayout(new BoxLayout(resultsPanel, BoxLayout.PAGE_AXIS));
resultsPanel.setBorder(BorderFactory.createEmptyBorder(0, 10, 10, 10));
resultsPanel.add(warnLabel, BorderLayout.PAGE_START);
resultsPanel.add(scroll, BorderLayout.CENTER);
resultsPanel.add(resultsActionsPanel, BorderLayout.PAGE_END);
return resultsPanel;
}
protected void addCustomResultsActions(JPanel actionsPanel) {
protected void addResultsActions(JPanel resultsActionsPanel) {
resultsActionsPanel.add(Box.createRigidArea(new Dimension(20, 0)));
resultsActionsPanel.add(resultsInfoLabel);
resultsActionsPanel.add(Box.createHorizontalGlue());
}
protected void updateProgressLabel(boolean complete) {
@@ -288,13 +295,12 @@ public abstract class CommonSearchDialog extends JFrame {
protected static final class ResultsTable extends JTable {
private static final long serialVersionUID = 3901184054736618969L;
private final transient ResultsTableCellRenderer renderer;
private final transient ResultsModel model;
public ResultsTable(ResultsModel resultsModel, ResultsTableCellRenderer renderer) {
super(resultsModel);
this.model = resultsModel;
this.renderer = renderer;
setRowHeight(renderer.getMaxRowHeight());
}
public void initColumnWidth() {
@@ -309,6 +315,11 @@ public abstract class CommonSearchDialog extends JFrame {
public void updateTable() {
UiUtils.uiThreadGuard();
int rowCount = getRowCount();
if (rowCount == 0) {
updateUI();
return;
}
long start = System.currentTimeMillis();
int width = getParent().getWidth();
TableColumn firstColumn = columnModel.getColumn(0);
@@ -325,30 +336,6 @@ public abstract class CommonSearchDialog extends JFrame {
} else {
firstColumn.setPreferredWidth(width);
}
int rowCount = getRowCount();
int columnCount = getColumnCount();
Map<Class<?>, Integer> heightByType = new HashMap<>();
for (int row = 0; row < rowCount; row++) {
Object value = model.getValueAt(row, 0);
Class<?> valueType = value.getClass();
Integer cachedHeight = heightByType.get(valueType);
if (cachedHeight != null) {
setRowHeight(row, cachedHeight);
} else {
int height = 0;
for (int col = 0; col < columnCount; col++) {
Component comp = prepareRenderer(renderer, row, col);
if (comp == null) {
continue;
}
Dimension preferredSize = comp.getPreferredSize();
int h = Math.max(comp.getHeight(), preferredSize.height);
height = Math.max(height, h);
}
heightByType.put(valueType, height);
setRowHeight(row, height);
}
}
updateUI();
if (LOG.isDebugEnabled()) {
LOG.debug("Update results table in {}ms, count: {}", System.currentTimeMillis() - start, rowCount);
@@ -365,14 +352,9 @@ public abstract class CommonSearchDialog extends JFrame {
private static final long serialVersionUID = -7821286846923903208L;
private static final String[] COLUMN_NAMES = { NLS.str("search_dialog.col_node"), NLS.str("search_dialog.col_code") };
private final transient List<JNode> rows = Collections.synchronizedList(new ArrayList<>());
private final transient ResultsTableCellRenderer renderer;
private final transient List<JNode> rows = new ArrayList<>();
private transient boolean addDescColumn;
public ResultsModel(ResultsTableCellRenderer renderer) {
this.renderer = renderer;
}
public void addAll(Collection<? extends JNode> nodes) {
rows.addAll(nodes);
if (!addDescColumn) {
@@ -388,7 +370,10 @@ public abstract class CommonSearchDialog extends JFrame {
public void clear() {
addDescColumn = false;
rows.clear();
renderer.clear();
}
public void sort() {
Collections.sort(rows);
}
public boolean isAddDescColumn() {
@@ -417,38 +402,39 @@ public abstract class CommonSearchDialog extends JFrame {
}
protected final class ResultsTableCellRenderer implements TableCellRenderer {
private final JLabel emptyLabel = new JLabel();
private final Font font;
private final JLabel label;
private final RSyntaxTextArea codeArea;
private final JLabel emptyLabel;
private final Color codeSelectedColor;
private final Color codeBackground;
private final Map<Integer, Component> componentCache = new HashMap<>();
public ResultsTableCellRenderer() {
RSyntaxTextArea area = AbstractCodeArea.getDefaultArea(mainWindow);
this.font = area.getFont();
this.codeSelectedColor = area.getSelectionColor();
this.codeBackground = area.getBackground();
codeArea = AbstractCodeArea.getDefaultArea(mainWindow);
codeArea.setBorder(BorderFactory.createEmptyBorder(0, 10, 0, 10));
codeArea.setRows(1);
codeBackground = codeArea.getBackground();
codeSelectedColor = codeArea.getSelectionColor();
label = new JLabel();
label.setOpaque(true);
label.setFont(codeArea.getFont());
label.setHorizontalAlignment(SwingConstants.LEFT);
emptyLabel = new JLabel();
emptyLabel.setOpaque(true);
}
@Override
public Component getTableCellRendererComponent(JTable table, Object obj,
boolean isSelected, boolean hasFocus, int row, int column) {
Component comp = componentCache.computeIfAbsent(makeID(row, column), id -> {
if (obj instanceof JNode) {
return makeCell((JNode) obj, column);
}
if (obj == null || table == null) {
return emptyLabel;
});
updateSelection(table, comp, isSelected);
}
Component comp = makeCell((JNode) obj, column);
updateSelection(table, comp, column, isSelected);
return comp;
}
private int makeID(int row, int col) {
return row << 2 | (col & 0b11);
}
private void updateSelection(JTable table, Component comp, boolean isSelected) {
if (comp instanceof RSyntaxTextArea) {
private void updateSelection(JTable table, Component comp, int column, boolean isSelected) {
if (column == 1) {
if (isSelected) {
comp.setBackground(codeSelectedColor);
} else {
@@ -467,39 +453,32 @@ public abstract class CommonSearchDialog extends JFrame {
private Component makeCell(JNode node, int column) {
if (column == 0) {
JLabel label = new JLabel(node.makeLongStringHtml(), node.getIcon(), SwingConstants.LEFT);
label.setFont(font);
label.setOpaque(true);
label.setText(node.makeLongStringHtml());
label.setToolTipText(label.getText());
label.setIcon(node.getIcon());
return label;
}
if (!node.hasDescString()) {
return emptyLabel;
}
RSyntaxTextArea textArea = AbstractCodeArea.getDefaultArea(mainWindow);
textArea.setSyntaxEditingStyle(node.getSyntaxName());
codeArea.setSyntaxEditingStyle(node.getSyntaxName());
String descStr = node.makeDescString();
textArea.setText(descStr);
if (descStr.contains("\n")) {
textArea.setRows(textArea.getLineCount());
} else {
textArea.setRows(1);
textArea.setColumns(descStr.length() + 1);
codeArea.setText(descStr);
codeArea.setColumns(descStr.length() + 1);
if (highlightContext != null) {
SearchEngine.markAll(codeArea, highlightContext);
}
if (highlightText != null) {
SearchContext searchContext = new SearchContext(highlightText);
searchContext.setMatchCase(!highlightTextCaseInsensitive);
searchContext.setRegularExpression(highlightTextUseRegex);
searchContext.setMarkAll(true);
SearchEngine.markAll(textArea, searchContext);
}
textArea.setBorder(BorderFactory.createEmptyBorder(0, 10, 0, 10));
return textArea;
return codeArea;
}
public void clear() {
componentCache.clear();
public int getMaxRowHeight() {
label.setText("Text");
codeArea.setText("Text");
return Math.max(getCompHeight(label), getCompHeight(codeArea));
}
private int getCompHeight(Component comp) {
return Math.max(comp.getHeight(), comp.getPreferredSize().height);
}
}
@@ -29,6 +29,7 @@ public class FileDialog {
public enum OpenMode {
OPEN,
OPEN_PROJECT,
ADD,
SAVE_PROJECT,
EXPORT,
@@ -95,13 +96,19 @@ public class FileDialog {
private void initForMode(OpenMode mode) {
switch (mode) {
case OPEN:
case OPEN_PROJECT:
case ADD:
fileExtList = new ArrayList<>(Arrays.asList("apk", "dex", "jar", "class", "smali", "zip", "aar", "arsc"));
if (mode == OpenMode.OPEN) {
fileExtList.addAll(Arrays.asList(JadxProject.PROJECT_EXTENSION, "aab"));
if (mode == OpenMode.OPEN_PROJECT) {
fileExtList = Collections.singletonList(JadxProject.PROJECT_EXTENSION);
title = NLS.str("file.open_title");
} else {
title = NLS.str("file.add_files_action");
fileExtList = new ArrayList<>(Arrays.asList("apk", "dex", "jar", "class", "smali", "zip", "xapk", "aar", "arsc"));
if (mode == OpenMode.OPEN) {
fileExtList.addAll(Arrays.asList(JadxProject.PROJECT_EXTENSION, "aab"));
title = NLS.str("file.open_title");
} else {
title = NLS.str("file.add_files_action");
}
}
selectionMode = JFileChooser.FILES_AND_DIRECTORIES;
currentDir = mainWindow.getSettings().getLastOpenFilePath();
@@ -2,11 +2,13 @@ package jadx.gui.ui.dialog;
import java.awt.BorderLayout;
import java.awt.Container;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import javax.swing.BorderFactory;
import javax.swing.JButton;
import javax.swing.JComboBox;
import javax.swing.JDialog;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
@@ -20,10 +22,11 @@ import jadx.gui.settings.JadxSettings;
import jadx.gui.ui.MainWindow;
import jadx.gui.ui.codearea.AbstractCodeArea;
import jadx.gui.utils.NLS;
import jadx.gui.utils.UiUtils;
import jadx.gui.utils.logs.ILogListener;
import jadx.gui.utils.logs.LogCollector;
public class LogViewerDialog extends JDialog {
public class LogViewerDialog extends JFrame {
private static final long serialVersionUID = -2188700277429054641L;
private static final Level[] LEVEL_ITEMS = { Level.DEBUG, Level.INFO, Level.WARN, Level.ERROR };
@@ -31,6 +34,9 @@ public class LogViewerDialog extends JDialog {
private final transient JadxSettings settings;
private transient RSyntaxTextArea textPane;
private JComboBox<Level> levelCb;
private static LogViewerDialog openLogDialog;
public static void open(MainWindow mainWindow) {
openWithLevel(mainWindow, level);
@@ -38,7 +44,16 @@ public class LogViewerDialog extends JDialog {
public static void openWithLevel(MainWindow mainWindow, Level newLevel) {
level = newLevel;
new LogViewerDialog(mainWindow).setVisible(true);
if (openLogDialog == null) {
LogViewerDialog newLogDialog = new LogViewerDialog(mainWindow);
newLogDialog.setVisible(true);
openLogDialog = newLogDialog;
} else {
LogViewerDialog logDialog = openLogDialog;
logDialog.levelCb.setSelectedItem(level);
logDialog.setVisible(true);
logDialog.toFront();
}
}
private LogViewerDialog(MainWindow mainWindow) {
@@ -46,25 +61,33 @@ public class LogViewerDialog extends JDialog {
initUI(mainWindow);
registerLogListener();
settings.loadWindowPos(this);
addWindowListener(new WindowAdapter() {
@Override
public void windowClosing(WindowEvent e) {
openLogDialog = null;
}
});
}
public final void initUI(MainWindow mainWindow) {
UiUtils.setWindowIcons(this);
textPane = AbstractCodeArea.getDefaultArea(mainWindow);
textPane.setBorder(BorderFactory.createEmptyBorder(15, 15, 15, 15));
JPanel controlPane = new JPanel();
controlPane.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5));
final JComboBox<Level> cb = new JComboBox<>(LEVEL_ITEMS);
cb.setSelectedItem(level);
cb.addActionListener(e -> {
int i = cb.getSelectedIndex();
levelCb = new JComboBox<>(LEVEL_ITEMS);
levelCb.setSelectedItem(level);
levelCb.addActionListener(e -> {
int i = levelCb.getSelectedIndex();
level = LEVEL_ITEMS[i];
registerLogListener();
});
JLabel levelLabel = new JLabel(NLS.str("log_viewer.log_level"));
levelLabel.setLabelFor(cb);
levelLabel.setLabelFor(levelCb);
controlPane.add(levelLabel);
controlPane.add(cb);
controlPane.add(levelCb);
JScrollPane scrollPane = new JScrollPane(textPane);
@@ -81,7 +104,6 @@ public class LogViewerDialog extends JDialog {
pack();
setSize(800, 600);
setDefaultCloseOperation(DISPOSE_ON_CLOSE);
setModalityType(ModalityType.MODELESS);
setLocationRelativeTo(null);
}
@@ -2,17 +2,16 @@ package jadx.gui.ui.dialog;
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Container;
import java.awt.Dimension;
import java.awt.FlowLayout;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.util.ArrayList;
import java.util.Collections;
import java.util.EnumSet;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import javax.swing.BorderFactory;
@@ -21,25 +20,29 @@ import javax.swing.BoxLayout;
import javax.swing.JButton;
import javax.swing.JCheckBox;
import javax.swing.JLabel;
import javax.swing.JMenuItem;
import javax.swing.JPanel;
import javax.swing.JPopupMenu;
import javax.swing.JTextField;
import javax.swing.WindowConstants;
import javax.swing.event.ChangeListener;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;
import org.jetbrains.annotations.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import hu.akarnokd.rxjava2.swing.SwingSchedulers;
import com.formdev.flatlaf.FlatClientProperties;
import com.formdev.flatlaf.icons.FlatSearchWithHistoryIcon;
import io.reactivex.BackpressureStrategy;
import io.reactivex.Emitter;
import io.reactivex.Flowable;
import io.reactivex.disposables.Disposable;
import io.reactivex.schedulers.Schedulers;
import jadx.api.JavaClass;
import jadx.core.utils.ListUtils;
import jadx.gui.jobs.ITaskInfo;
import jadx.gui.jobs.ITaskProgress;
import jadx.gui.search.SearchSettings;
import jadx.gui.search.SearchTask;
@@ -58,6 +61,7 @@ import jadx.gui.utils.NLS;
import jadx.gui.utils.TextStandardActions;
import jadx.gui.utils.UiUtils;
import jadx.gui.utils.layout.WrapLayout;
import jadx.gui.utils.rx.RxUtils;
import static jadx.gui.ui.dialog.SearchDialog.SearchOptions.ACTIVE_TAB;
import static jadx.gui.ui.dialog.SearchDialog.SearchOptions.CLASS;
@@ -77,17 +81,28 @@ public class SearchDialog extends CommonSearchDialog {
public static void search(MainWindow window, SearchPreset preset) {
SearchDialog searchDialog = new SearchDialog(window, preset, Collections.emptySet());
searchDialog.setVisible(true);
show(searchDialog, window);
}
public static void searchInActiveTab(MainWindow window, SearchPreset preset) {
SearchDialog searchDialog = new SearchDialog(window, preset, EnumSet.of(SearchOptions.ACTIVE_TAB));
searchDialog.setVisible(true);
show(searchDialog, window);
}
public static void searchText(MainWindow window, String text) {
SearchDialog searchDialog = new SearchDialog(window, SearchPreset.TEXT, Collections.emptySet());
searchDialog.initSearchText = text;
show(searchDialog, window);
}
private static void show(SearchDialog searchDialog, MainWindow mw) {
mw.addLoadListener(loaded -> {
if (!loaded) {
searchDialog.dispose();
return true;
}
return false;
});
searchDialog.setVisible(true);
}
@@ -118,6 +133,8 @@ public class SearchDialog extends CommonSearchDialog {
private transient @Nullable SearchTask searchTask;
private transient JButton loadAllButton;
private transient JButton loadMoreButton;
private transient JButton stopBtn;
private transient JButton sortBtn;
private transient Disposable searchDisposable;
private transient SearchEventEmitter searchEmitter;
@@ -128,6 +145,11 @@ public class SearchDialog extends CommonSearchDialog {
// temporal list for pending results
private final List<JNode> pendingResults = new ArrayList<>();
/**
* Use single thread to do all background work, so additional synchronisation not needed
*/
private final Executor searchBackgroundExecutor = Executors.newSingleThreadExecutor();
private SearchDialog(MainWindow mainWindow, SearchPreset preset, Set<SearchOptions> additionalOptions) {
super(mainWindow, NLS.str("menu.text_search"));
this.searchPreset = preset;
@@ -136,7 +158,7 @@ public class SearchDialog extends CommonSearchDialog {
loadWindowPos();
initUI();
searchFieldSubscribe();
initSearchEvents();
registerInitOnOpen();
registerActiveTabListener();
}
@@ -148,13 +170,10 @@ public class SearchDialog extends CommonSearchDialog {
}
resultsModel.clear();
removeActiveTabListener();
if (searchTask != null) {
searchTask.cancel();
mainWindow.getBackgroundExecutor().execute(NLS.str("progress.load"), () -> {
stopSearchTask();
unloadTempData();
});
}
searchBackgroundExecutor.execute(() -> {
stopSearchTask();
unloadTempData();
});
super.dispose();
}
@@ -167,7 +186,7 @@ public class SearchDialog extends CommonSearchDialog {
case TEXT:
if (searchOptions.isEmpty()) {
searchOptions.add(SearchOptions.CODE);
searchOptions.add(SearchOptions.IGNORE_CASE);
searchOptions.add(IGNORE_CASE);
}
break;
@@ -204,17 +223,44 @@ public class SearchDialog extends CommonSearchDialog {
searchFieldDefaultBgColor = searchField.getBackground();
searchField.setAlignmentX(LEFT_ALIGNMENT);
TextStandardActions.attach(searchField);
addSearchHistoryButton();
searchField.putClientProperty(FlatClientProperties.TEXT_FIELD_SHOW_CLEAR_BUTTON, true);
boolean autoSearch = mainWindow.getSettings().isUseAutoSearch();
JButton searchBtn = new JButton(NLS.str("search_dialog.search_button"));
searchBtn.setVisible(!autoSearch);
searchBtn.addActionListener(ev -> searchEmitter.emitSearch());
JCheckBox autoSearchCB = new JCheckBox(NLS.str("search_dialog.auto_search"));
autoSearchCB.setSelected(autoSearch);
autoSearchCB.addActionListener(ev -> {
boolean newValue = autoSearchCB.isSelected();
mainWindow.getSettings().setUseAutoSearch(newValue);
searchBtn.setVisible(!newValue);
initSearchEvents();
if (newValue) {
searchEmitter.emitSearch();
}
});
JPanel searchLinePanel = new JPanel();
searchLinePanel.setLayout(new BoxLayout(searchLinePanel, BoxLayout.LINE_AXIS));
searchLinePanel.add(searchField);
searchLinePanel.add(Box.createRigidArea(new Dimension(5, 0)));
searchLinePanel.add(searchBtn);
searchLinePanel.add(Box.createRigidArea(new Dimension(5, 0)));
searchLinePanel.add(autoSearchCB);
searchLinePanel.setAlignmentX(LEFT_ALIGNMENT);
JLabel findLabel = new JLabel(NLS.str("search_dialog.open_by_name"));
findLabel.setAlignmentX(LEFT_ALIGNMENT);
JPanel searchFieldPanel = new JPanel();
searchFieldPanel.setLayout(new BoxLayout(searchFieldPanel, BoxLayout.PAGE_AXIS));
searchFieldPanel.setBorder(BorderFactory.createEmptyBorder(0, 5, 0, 5));
searchFieldPanel.setAlignmentX(LEFT_ALIGNMENT);
searchFieldPanel.add(findLabel);
searchFieldPanel.add(Box.createRigidArea(new Dimension(0, 5)));
searchFieldPanel.add(searchField);
searchFieldPanel.add(searchLinePanel);
JPanel searchInPanel = new JPanel(new FlowLayout(FlowLayout.LEFT));
searchInPanel.setBorder(BorderFactory.createTitledBorder(NLS.str("search_dialog.search_in")));
@@ -227,18 +273,17 @@ public class SearchDialog extends CommonSearchDialog {
JPanel searchOptions = new JPanel(new FlowLayout(FlowLayout.LEFT));
searchOptions.setBorder(BorderFactory.createTitledBorder(NLS.str("search_dialog.options")));
searchOptions.add(makeOptionsCheckBox(NLS.str("search_dialog.ignorecase"), SearchOptions.IGNORE_CASE));
searchOptions.add(makeOptionsCheckBox(NLS.str("search_dialog.regex"), SearchOptions.USE_REGEX));
searchOptions.add(makeOptionsCheckBox(NLS.str("search_dialog.ignorecase"), IGNORE_CASE));
searchOptions.add(makeOptionsCheckBox(NLS.str("search_dialog.regex"), USE_REGEX));
searchOptions.add(makeOptionsCheckBox(NLS.str("search_dialog.active_tab"), SearchOptions.ACTIVE_TAB));
JPanel optionsPanel = new JPanel(new WrapLayout(WrapLayout.LEFT));
JPanel optionsPanel = new JPanel(new WrapLayout(WrapLayout.LEFT, 0, 0));
optionsPanel.setAlignmentX(LEFT_ALIGNMENT);
optionsPanel.add(searchInPanel);
optionsPanel.add(searchOptions);
JPanel searchPane = new JPanel();
searchPane.setLayout(new BoxLayout(searchPane, BoxLayout.PAGE_AXIS));
searchPane.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10));
searchPane.add(searchFieldPanel);
searchPane.add(Box.createRigidArea(new Dimension(0, 5)));
searchPane.add(optionsPanel);
@@ -247,38 +292,69 @@ public class SearchDialog extends CommonSearchDialog {
JPanel resultsPanel = initResultsTable();
JPanel buttonPane = initButtonsPanel();
Container contentPane = getContentPane();
contentPane.add(searchPane, BorderLayout.PAGE_START);
contentPane.add(resultsPanel, BorderLayout.CENTER);
contentPane.add(buttonPane, BorderLayout.PAGE_END);
JPanel contentPanel = new JPanel();
contentPanel.setLayout(new BorderLayout(5, 5));
contentPanel.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10));
contentPanel.add(searchPane, BorderLayout.PAGE_START);
contentPanel.add(resultsPanel, BorderLayout.CENTER);
contentPanel.add(buttonPane, BorderLayout.PAGE_END);
getContentPane().add(contentPanel);
searchField.addKeyListener(new KeyAdapter() {
@Override
public void keyReleased(KeyEvent e) {
if (e.getKeyCode() == KeyEvent.VK_ENTER) {
if (resultsModel.getRowCount() != 0) {
resultsTable.setRowSelectionInterval(0, 0);
}
resultsTable.requestFocus();
}
}
});
setLocationRelativeTo(null);
setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE);
}
protected void addCustomResultsActions(JPanel resultsActionsPanel) {
private void addSearchHistoryButton() {
JButton searchHistoryButton = new JButton(new FlatSearchWithHistoryIcon(true));
searchHistoryButton.setToolTipText(NLS.str("search_dialog.search_history"));
searchHistoryButton.addActionListener(e -> {
JPopupMenu popupMenu = new JPopupMenu();
List<String> searchHistory = mainWindow.getProject().getSearchHistory();
if (searchHistory.isEmpty()) {
popupMenu.add("(empty)");
} else {
for (String str : searchHistory) {
JMenuItem item = popupMenu.add(str);
item.addActionListener(ev -> searchField.setText(str));
}
}
popupMenu.show(searchHistoryButton, 0, searchHistoryButton.getHeight());
});
searchField.putClientProperty(FlatClientProperties.TEXT_FIELD_LEADING_COMPONENT, searchHistoryButton);
}
protected void addResultsActions(JPanel resultsActionsPanel) {
loadAllButton = new JButton(NLS.str("search_dialog.load_all"));
loadAllButton.addActionListener(e -> loadAll());
loadAllButton.addActionListener(e -> loadMoreResults(true));
loadAllButton.setEnabled(false);
loadMoreButton = new JButton(NLS.str("search_dialog.load_more"));
loadMoreButton.addActionListener(e -> loadMore());
loadMoreButton.addActionListener(e -> loadMoreResults(false));
loadMoreButton.setEnabled(false);
stopBtn = new JButton(NLS.str("search_dialog.stop"));
stopBtn.addActionListener(e -> pauseSearch());
stopBtn.setEnabled(false);
sortBtn = new JButton(NLS.str("search_dialog.sort_results"));
sortBtn.addActionListener(e -> {
synchronized (pendingResults) {
resultsModel.sort();
resultsTable.updateTable();
}
});
sortBtn.setEnabled(false);
resultsActionsPanel.add(loadAllButton);
resultsActionsPanel.add(Box.createRigidArea(new Dimension(10, 0)));
resultsActionsPanel.add(loadMoreButton);
resultsActionsPanel.add(Box.createRigidArea(new Dimension(10, 0)));
resultsActionsPanel.add(stopBtn);
resultsActionsPanel.add(Box.createRigidArea(new Dimension(10, 0)));
resultsActionsPanel.add(stopBtn);
super.addResultsActions(resultsActionsPanel);
resultsActionsPanel.add(Box.createRigidArea(new Dimension(10, 0)));
resultsActionsPanel.add(sortBtn);
}
private class SearchEventEmitter {
@@ -302,26 +378,50 @@ public class SearchDialog extends CommonSearchDialog {
}
}
private void searchFieldSubscribe() {
private void initSearchEvents() {
if (searchDisposable != null) {
searchDisposable.dispose();
searchDisposable = null;
}
searchEmitter = new SearchEventEmitter();
Flowable<String> textChanges = onTextFieldChanges(searchField);
Flowable<String> searchEvents = Flowable.merge(textChanges, searchEmitter.getFlowable());
Flowable<String> searchEvents;
if (mainWindow.getSettings().isUseAutoSearch()) {
searchEvents = Flowable.merge(RxUtils.textFieldChanges(searchField),
RxUtils.textFieldEnterPress(searchField), searchEmitter.getFlowable());
} else {
searchEvents = Flowable.merge(RxUtils.textFieldEnterPress(searchField), searchEmitter.getFlowable());
}
searchDisposable = searchEvents
.debounce(500, TimeUnit.MILLISECONDS)
.observeOn(SwingSchedulers.edt())
.debounce(50, TimeUnit.MILLISECONDS)
.observeOn(Schedulers.from(searchBackgroundExecutor))
.subscribe(this::search);
}
@Nullable
private synchronized void search(String text) {
UiUtils.uiThreadGuard();
resetSearch();
if (text == null || options.isEmpty()) {
private void search(String text) {
UiUtils.notUiThreadGuard();
stopSearchTask();
UiUtils.uiRun(this::resetSearch);
searchTask = prepareSearch(text);
if (searchTask == null) {
return;
}
UiUtils.uiRunAndWait(() -> {
updateTableHighlight();
prepareForSearch();
});
this.searchTask.setResultsLimit(50);
this.searchTask.setProgressListener(this::updateProgress);
this.searchTask.fetchResults();
LOG.debug("Total search items count estimation: {}", this.searchTask.getTaskProgress().total());
}
private SearchTask prepareSearch(String text) {
if (text == null || options.isEmpty()) {
return null;
}
// allow empty text for comments search
if (text.isEmpty() && !options.contains(SearchOptions.COMMENT)) {
return;
return null;
}
LOG.debug("Building search for '{}', options: {}", text, options);
boolean ignoreCase = options.contains(IGNORE_CASE);
@@ -335,24 +435,16 @@ public class SearchDialog extends CommonSearchDialog {
} else {
searchField.setBackground(SEARCH_FIELD_ERROR_COLOR);
resultsInfoLabel.setText(error);
return;
return null;
}
searchTask = new SearchTask(mainWindow, this::addSearchResult, s -> searchComplete());
if (!buildSearch(text, searchSettings)) {
return;
SearchTask newSearchTask = new SearchTask(mainWindow, this::addSearchResult, this::searchFinished);
if (!buildSearch(newSearchTask, text, searchSettings)) {
return null;
}
updateTableHighlight();
startSearch();
searchTask.setResultsLimit(100);
searchTask.setProgressListener(this::updateProgress);
searchTask.fetchResults();
LOG.debug("Total search items count estimation: {}", searchTask.getTaskProgress().total());
return newSearchTask;
}
private boolean buildSearch(String text, SearchSettings searchSettings) {
Objects.requireNonNull(searchTask);
private boolean buildSearch(SearchTask newSearchTask, String text, SearchSettings searchSettings) {
List<JavaClass> allClasses;
if (options.contains(ACTIVE_TAB)) {
JumpPosition currentPos = mainWindow.getTabbedPane().getCurrentPosition();
@@ -368,7 +460,7 @@ public class SearchDialog extends CommonSearchDialog {
}
// allow empty text for comments search
if (text.isEmpty() && options.contains(SearchOptions.COMMENT)) {
searchTask.addProviderJob(new CommentSearchProvider(mainWindow, searchSettings));
newSearchTask.addProviderJob(new CommentSearchProvider(mainWindow, searchSettings));
return true;
}
// using ordered execution for fast tasks
@@ -384,86 +476,119 @@ public class SearchDialog extends CommonSearchDialog {
}
if (options.contains(CODE)) {
if (allClasses.size() == 1) {
searchTask.addProviderJob(new CodeSearchProvider(mainWindow, searchSettings, allClasses));
newSearchTask.addProviderJob(new CodeSearchProvider(mainWindow, searchSettings, allClasses));
} else {
List<JavaClass> topClasses = ListUtils.filter(allClasses, c -> !c.isInner());
for (List<JavaClass> batch : mainWindow.getWrapper().buildDecompileBatches(topClasses)) {
searchTask.addProviderJob(new CodeSearchProvider(mainWindow, searchSettings, batch));
List<List<JavaClass>> batches = mainWindow.getCacheObject().getDecompileBatches();
if (batches == null) {
List<JavaClass> topClasses = ListUtils.filter(allClasses, c -> !c.isInner());
batches = mainWindow.getWrapper().buildDecompileBatches(topClasses);
mainWindow.getCacheObject().setDecompileBatches(batches);
}
for (List<JavaClass> batch : batches) {
newSearchTask.addProviderJob(new CodeSearchProvider(mainWindow, searchSettings, batch));
}
}
}
if (options.contains(RESOURCE)) {
searchTask.addProviderJob(new ResourceSearchProvider(mainWindow, searchSettings));
newSearchTask.addProviderJob(new ResourceSearchProvider(mainWindow, searchSettings));
}
if (options.contains(COMMENT)) {
searchTask.addProviderJob(new CommentSearchProvider(mainWindow, searchSettings));
newSearchTask.addProviderJob(new CommentSearchProvider(mainWindow, searchSettings));
}
merged.prepare();
searchTask.addProviderJob(merged);
newSearchTask.addProviderJob(merged);
return true;
}
private synchronized void stopSearchTask() {
@Override
protected void openItem(JNode node) {
if (mainWindow.getSettings().isUseAutoSearch()) {
// for auto search save only searches which leads to node opening
mainWindow.getProject().addToSearchHistory(searchField.getText());
}
super.openItem(node);
}
private void pauseSearch() {
stopBtn.setEnabled(false);
searchBackgroundExecutor.execute(() -> {
if (searchTask != null) {
searchTask.cancel();
}
});
}
private void stopSearchTask() {
UiUtils.notUiThreadGuard();
if (searchTask != null) {
searchTask.cancel();
searchTask.waitTask();
searchTask = null;
}
}
private synchronized void loadMore() {
if (searchTask == null) {
return;
}
startSearch();
searchTask.fetchResults();
private void loadMoreResults(boolean all) {
searchBackgroundExecutor.execute(() -> {
if (searchTask == null) {
return;
}
searchTask.cancel();
searchTask.waitTask();
UiUtils.uiRunAndWait(this::prepareForSearch);
if (all) {
searchTask.setResultsLimit(0);
}
searchTask.fetchResults();
});
}
private synchronized void loadAll() {
if (searchTask == null) {
return;
}
startSearch();
searchTask.setResultsLimit(0);
searchTask.fetchResults();
}
private synchronized void resetSearch() {
private void resetSearch() {
UiUtils.uiThreadGuard();
resultsModel.clear();
updateTable();
resultsTable.updateTable();
synchronized (pendingResults) {
pendingResults.clear();
}
progressPane.setVisible(false);
warnLabel.setVisible(false);
loadAllButton.setEnabled(false);
loadMoreButton.setEnabled(false);
stopSearchTask();
}
private void startSearch() {
private void prepareForSearch() {
UiUtils.uiThreadGuard();
stopBtn.setEnabled(true);
sortBtn.setEnabled(false);
showSearchState();
progressStartCommon();
}
private void addSearchResult(JNode node) {
Objects.requireNonNull(node);
synchronized (pendingResults) {
UiUtils.notUiThreadGuard();
pendingResults.add(node);
}
}
private void updateTable() {
synchronized (pendingResults) {
UiUtils.uiThreadGuard();
Collections.sort(pendingResults);
resultsModel.addAll(pendingResults);
pendingResults.clear();
resultsTable.updateTable();
}
resultsTable.updateTable();
}
private void updateTableHighlight() {
String text = searchField.getText();
setHighlightText(text);
highlightTextCaseInsensitive = options.contains(SearchOptions.IGNORE_CASE);
highlightTextUseRegex = options.contains(SearchOptions.USE_REGEX);
updateHighlightContext(text, !options.contains(IGNORE_CASE), options.contains(USE_REGEX));
cache.setLastSearch(text);
cache.getLastSearchOptions().put(searchPreset, options);
if (!mainWindow.getSettings().isUseAutoSearch()) {
mainWindow.getProject().addToSearchHistory(text);
}
}
private void updateProgress(ITaskProgress progress) {
@@ -473,17 +598,16 @@ public class SearchDialog extends CommonSearchDialog {
});
}
private synchronized void searchComplete() {
private void searchFinished(ITaskInfo status, Boolean complete) {
UiUtils.uiThreadGuard();
LOG.debug("Search complete");
updateTable();
boolean complete = searchTask == null || searchTask.isSearchComplete();
LOG.debug("Search complete: {}, complete: {}", status, complete);
loadAllButton.setEnabled(!complete);
loadMoreButton.setEnabled(!complete);
updateProgressLabel(complete);
unloadTempData();
stopBtn.setEnabled(false);
progressFinishedCommon();
updateTable();
updateProgressLabel(complete);
sortBtn.setEnabled(resultsModel.getRowCount() != 0);
}
private void unloadTempData() {
@@ -491,48 +615,6 @@ public class SearchDialog extends CommonSearchDialog {
System.gc();
}
private static Flowable<String> onTextFieldChanges(final JTextField textField) {
return Flowable.<String>create(emitter -> {
DocumentListener listener = new DocumentListener() {
@Override
public void insertUpdate(DocumentEvent e) {
change();
}
@Override
public void removeUpdate(DocumentEvent e) {
change();
}
@Override
public void changedUpdate(DocumentEvent e) {
change();
}
public void change() {
emitter.onNext(textField.getText());
}
};
textField.getDocument().addDocumentListener(listener);
emitter.setDisposable(new Disposable() {
private boolean disposed = false;
@Override
public void dispose() {
textField.getDocument().removeDocumentListener(listener);
disposed = true;
}
@Override
public boolean isDisposed() {
return disposed;
}
});
}, BackpressureStrategy.LATEST)
.distinctUntilChanged();
}
private JCheckBox makeOptionsCheckBox(String name, final SearchOptions opt) {
final JCheckBox chBox = new JCheckBox(name);
chBox.setAlignmentX(LEFT_ALIGNMENT);
@@ -1,7 +1,6 @@
package jadx.gui.ui.dialog;
import java.awt.BorderLayout;
import java.awt.Container;
import java.awt.FlowLayout;
import java.awt.Font;
import java.util.ArrayList;
@@ -83,6 +82,15 @@ public class UsageDialog extends CommonSearchDialog {
for (JavaMethod mth : getMethodWithOverrides(javaMethod)) {
map.put(mth, mth.getUseIn());
}
} else if (node instanceof JClass) {
JavaClass javaCls = ((JClass) node).getCls();
map.put(javaCls, javaCls.getUseIn());
// add constructors usage into class usage
for (JavaMethod javaMth : javaCls.getMethods()) {
if (javaMth.isConstructor()) {
map.put(javaMth, javaMth.getUseIn());
}
}
} else {
JavaNode javaNode = node.getJavaNode();
map.put(javaNode, javaNode.getUseIn());
@@ -123,8 +131,7 @@ public class UsageDialog extends CommonSearchDialog {
Collections.sort(usageList);
resultsModel.addAll(usageList);
// TODO: highlight only needed node usage
setHighlightText(null);
updateHighlightContext(node.getName(), true, false);
resultsTable.initColumnWidth();
resultsTable.updateTable();
updateProgressLabel(true);
@@ -154,10 +161,13 @@ public class UsageDialog extends CommonSearchDialog {
JPanel resultsPanel = initResultsTable();
JPanel buttonPane = initButtonsPanel();
Container contentPane = getContentPane();
contentPane.add(searchPane, BorderLayout.PAGE_START);
contentPane.add(resultsPanel, BorderLayout.CENTER);
contentPane.add(buttonPane, BorderLayout.PAGE_END);
JPanel contentPanel = new JPanel();
contentPanel.setLayout(new BorderLayout(5, 5));
contentPanel.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10));
contentPanel.add(searchPane, BorderLayout.PAGE_START);
contentPanel.add(resultsPanel, BorderLayout.CENTER);
contentPanel.add(buttonPane, BorderLayout.PAGE_END);
getContentPane().add(contentPanel);
pack();
setSize(800, 500);
@@ -0,0 +1,127 @@
package jadx.gui.ui.panel;
import java.awt.BorderLayout;
import java.awt.Dimension;
import java.awt.Font;
import java.nio.file.Path;
import java.util.List;
import javax.swing.BorderFactory;
import javax.swing.Box;
import javax.swing.BoxLayout;
import javax.swing.JButton;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.ScrollPaneConstants;
import javax.swing.border.Border;
import javax.swing.border.TitledBorder;
import jadx.gui.settings.JadxSettings;
import jadx.gui.ui.MainWindow;
import jadx.gui.ui.TabbedPane;
import jadx.gui.ui.treenodes.StartPageNode;
import jadx.gui.utils.Icons;
import jadx.gui.utils.NLS;
public class StartPagePanel extends ContentPanel {
public StartPagePanel(TabbedPane tabbedPane, StartPageNode node) {
super(tabbedPane, node);
MainWindow mainWindow = tabbedPane.getMainWindow();
Font baseFont = mainWindow.getSettings().getFont();
JButton openFile = new JButton(NLS.str("file.open_title"), Icons.OPEN);
openFile.addActionListener(ev -> mainWindow.openFileDialog());
JButton openProject = new JButton(NLS.str("file.open_project"), Icons.OPEN_PROJECT);
openProject.addActionListener(ev -> mainWindow.openProjectDialog());
JPanel start = new JPanel();
start.setBorder(sectionFrame(NLS.str("start_page.start"), baseFont));
start.setLayout(new BoxLayout(start, BoxLayout.LINE_AXIS));
start.add(openFile);
start.add(Box.createRigidArea(new Dimension(10, 0)));
start.add(openProject);
start.add(Box.createHorizontalGlue());
JPanel recentPanel = new JPanel();
JScrollPane scrollPane = new JScrollPane(recentPanel);
scrollPane.setHorizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_AS_NEEDED);
scrollPane.setVerticalScrollBarPolicy(ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED);
scrollPane.setPreferredSize(new Dimension(400, 200));
scrollPane.setBorder(BorderFactory.createEmptyBorder());
fillRecentPanel(recentPanel, scrollPane, mainWindow);
JPanel recent = new JPanel();
recent.setBorder(sectionFrame(NLS.str("start_page.recent"), baseFont));
recent.setLayout(new BoxLayout(recent, BoxLayout.PAGE_AXIS));
recent.add(scrollPane);
JPanel center = new JPanel();
center.setLayout(new BorderLayout(10, 10));
center.add(start, BorderLayout.PAGE_START);
center.add(recent, BorderLayout.CENTER);
center.setMaximumSize(new Dimension(700, 600));
center.setAlignmentX(CENTER_ALIGNMENT);
setLayout(new BoxLayout(this, BoxLayout.PAGE_AXIS));
setBorder(BorderFactory.createEmptyBorder(50, 50, 50, 50));
add(Box.createVerticalGlue());
add(center);
add(Box.createVerticalGlue());
}
private void fillRecentPanel(JPanel panel, JScrollPane scrollPane, MainWindow mainWindow) {
JadxSettings settings = mainWindow.getSettings();
List<Path> recentProjects = settings.getRecentProjects();
panel.removeAll();
panel.setLayout(new BoxLayout(panel, BoxLayout.Y_AXIS));
Font baseFont = settings.getFont();
Font font = baseFont.deriveFont(baseFont.getSize() - 1f);
for (Path path : recentProjects) {
JButton openBtn = new JButton(path.getFileName().toString());
openBtn.setToolTipText(path.toAbsolutePath().toString());
openBtn.setFont(font);
openBtn.setBorderPainted(false);
openBtn.addActionListener(ev -> mainWindow.open(path));
JButton removeBtn = new JButton();
removeBtn.setIcon(Icons.CLOSE_INACTIVE);
removeBtn.setRolloverIcon(Icons.CLOSE);
removeBtn.setRolloverEnabled(true);
removeBtn.setFocusable(false);
removeBtn.setBorder(null);
removeBtn.setBorderPainted(false);
removeBtn.setContentAreaFilled(false);
removeBtn.setOpaque(true);
removeBtn.addActionListener(e -> {
mainWindow.getSettings().removeRecentProject(path);
fillRecentPanel(panel, scrollPane, mainWindow);
panel.revalidate();
scrollPane.repaint();
});
JPanel linePanel = new JPanel();
linePanel.setLayout(new BoxLayout(linePanel, BoxLayout.LINE_AXIS));
linePanel.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5));
linePanel.add(openBtn);
linePanel.add(Box.createHorizontalGlue());
linePanel.add(removeBtn);
panel.add(linePanel);
}
panel.add(Box.createVerticalGlue());
}
private static Border sectionFrame(String title, Font font) {
TitledBorder titledBorder = BorderFactory.createTitledBorder(title);
titledBorder.setTitleFont(font.deriveFont(Font.BOLD, font.getSize() + 1));
Border spacing = BorderFactory.createEmptyBorder(10, 10, 10, 10);
return BorderFactory.createCompoundBorder(titledBorder, spacing);
}
@Override
public void loadSettings() {
}
}
@@ -0,0 +1,35 @@
package jadx.gui.ui.treenodes;
import javax.swing.Icon;
import jadx.gui.treemodel.JClass;
import jadx.gui.treemodel.JNode;
import jadx.gui.ui.TabbedPane;
import jadx.gui.ui.panel.ContentPanel;
import jadx.gui.ui.panel.StartPagePanel;
import jadx.gui.utils.Icons;
import jadx.gui.utils.NLS;
public class StartPageNode extends JNode {
private static final long serialVersionUID = 8983134608645736174L;
@Override
public ContentPanel getContentPanel(TabbedPane tabbedPane) {
return new StartPagePanel(tabbedPane, this);
}
@Override
public String makeString() {
return NLS.str("start_page.title");
}
@Override
public Icon getIcon() {
return Icons.START_PAGE;
}
@Override
public JClass getJParent() {
return null;
}
}
@@ -1,13 +1,13 @@
package jadx.gui.utils;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.jetbrains.annotations.Nullable;
import jadx.gui.settings.JadxSettings;
import jadx.gui.treemodel.JRoot;
import jadx.api.JavaClass;
import jadx.gui.ui.dialog.SearchDialog;
public class CacheObject {
@@ -16,19 +16,17 @@ public class CacheObject {
private JNodeCache jNodeCache;
private Map<SearchDialog.SearchPreset, Set<SearchDialog.SearchOptions>> lastSearchOptions;
private JRoot jRoot;
private JadxSettings settings;
private List<List<JavaClass>> decompileBatches;
public CacheObject() {
reset();
}
public void reset() {
jRoot = null;
settings = null;
lastSearch = null;
jNodeCache = new JNodeCache();
lastSearchOptions = new HashMap<>();
decompileBatches = null;
}
@Nullable
@@ -48,19 +46,11 @@ public class CacheObject {
return lastSearchOptions;
}
public void setJadxSettings(JadxSettings settings) {
this.settings = settings;
public @Nullable List<List<JavaClass>> getDecompileBatches() {
return decompileBatches;
}
public JadxSettings getJadxSettings() {
return this.settings;
}
public JRoot getJRoot() {
return jRoot;
}
public void setJRoot(JRoot jRoot) {
this.jRoot = jRoot;
public void setDecompileBatches(List<List<JavaClass>> decompileBatches) {
this.decompileBatches = decompileBatches;
}
}
@@ -0,0 +1,11 @@
package jadx.gui.utils;
public interface ILoadListener {
/**
* Update files/project loaded state
*
* @return true to remove listener
*/
boolean update(boolean loaded);
}
@@ -0,0 +1,20 @@
package jadx.gui.utils;
import javax.swing.ImageIcon;
import static jadx.gui.utils.UiUtils.openSvgIcon;
public class Icons {
public static final ImageIcon OPEN = openSvgIcon("ui/openDisk");
public static final ImageIcon OPEN_PROJECT = openSvgIcon("ui/projectDirectory");
public static final ImageIcon NEW_PROJECT = openSvgIcon("ui/newFolder");
public static final ImageIcon CLOSE = openSvgIcon("ui/closeHovered");
public static final ImageIcon CLOSE_INACTIVE = openSvgIcon("ui/close");
public static final ImageIcon STATIC = openSvgIcon("nodes/staticMark");
public static final ImageIcon FINAL = openSvgIcon("nodes/finalMark");
public static final ImageIcon START_PAGE = openSvgIcon("nodes/newWindow");
}
@@ -37,6 +37,7 @@ public class NLS {
LANG_LOCALES.add(new LangLocale("es", "ES"));
LANG_LOCALES.add(new LangLocale("de", "DE"));
LANG_LOCALES.add(new LangLocale("ko", "KR"));
LANG_LOCALES.add(new LangLocale("pt", "BR"));
LANG_LOCALES.forEach(NLS::load);
@@ -46,9 +46,6 @@ import jadx.gui.ui.codearea.AbstractCodeArea;
public class UiUtils {
private static final Logger LOG = LoggerFactory.getLogger(UiUtils.class);
public static final ImageIcon ICON_STATIC = openSvgIcon("nodes/staticMark");
public static final ImageIcon ICON_FINAL = openSvgIcon("nodes/finalMark");
/**
* The minimum about of memory in bytes we are trying to keep free, otherwise the application may
* run out of heap
@@ -190,10 +187,10 @@ public class UiUtils {
}
OverlayIcon overIcon = new OverlayIcon(icon);
if (af.isFinal()) {
overIcon.add(ICON_FINAL);
overIcon.add(Icons.FINAL);
}
if (af.isStatic()) {
overIcon.add(ICON_STATIC);
overIcon.add(Icons.STATIC);
}
return overIcon;
}
@@ -377,6 +374,8 @@ public class UiUtils {
}
try {
SwingUtilities.invokeAndWait(runnable);
} catch (InterruptedException e) {
LOG.warn("UI thread interrupted, runnable: {}", runnable, e);
} catch (Exception e) {
throw new RuntimeException(e);
}
@@ -388,6 +387,12 @@ public class UiUtils {
}
}
public static void notUiThreadGuard() {
if (SwingUtilities.isEventDispatchThread()) {
LOG.warn("Expect background thread, got: {}", Thread.currentThread(), new JadxRuntimeException());
}
}
@TestOnly
public static void debugTimer(int periodInSeconds, Runnable action) {
if (!LOG.isDebugEnabled()) {
@@ -15,6 +15,7 @@ import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.attribute.FileTime;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
@@ -30,6 +31,7 @@ import jadx.api.ICodeCache;
import jadx.api.ICodeInfo;
import jadx.api.JadxArgs;
import jadx.core.Jadx;
import jadx.core.dex.nodes.ClassNode;
import jadx.core.dex.nodes.RootNode;
import jadx.core.utils.Utils;
import jadx.core.utils.exceptions.JadxRuntimeException;
@@ -42,7 +44,7 @@ import static java.nio.file.StandardOpenOption.WRITE;
public class DiskCodeCache implements ICodeCache {
private static final Logger LOG = LoggerFactory.getLogger(DiskCodeCache.class);
private static final int DATA_FORMAT_VERSION = 12;
private static final int DATA_FORMAT_VERSION = 13;
private static final byte[] JADX_NAMES_MAP_HEADER = "jadxnm".getBytes(StandardCharsets.US_ASCII);
@@ -55,6 +57,7 @@ public class DiskCodeCache implements ICodeCache {
private final ExecutorService writePool;
private final Map<String, ICodeInfo> writeOps = new ConcurrentHashMap<>();
private final Map<String, Integer> namesMap = new ConcurrentHashMap<>();
private final Map<String, Integer> allClsIds;
public DiskCodeCache(RootNode root, Path baseDir) {
srcDir = baseDir.resolve("sources");
@@ -65,6 +68,7 @@ public class DiskCodeCache implements ICodeCache {
codeVersion = buildCodeVersion(args);
writePool = Executors.newFixedThreadPool(args.getThreadsCount());
codeMetadataAdapter = new CodeMetadataAdapter(root);
allClsIds = buildClassIdsMap(root.getClasses());
if (checkCodeVersion()) {
loadNamesMap();
} else {
@@ -112,6 +116,7 @@ public class DiskCodeCache implements ICodeCache {
public void add(String clsFullName, ICodeInfo codeInfo) {
writeOps.put(clsFullName, codeInfo);
int clsId = getClsId(clsFullName);
namesMap.put(clsFullName, clsId);
writePool.execute(() -> {
try {
FileUtils.writeFile(getJavaFile(clsId), codeInfo.getCodeStr());
@@ -218,7 +223,11 @@ public class DiskCodeCache implements ICodeCache {
}
private int getClsId(String clsFullName) {
return namesMap.computeIfAbsent(clsFullName, n -> namesMap.size());
Integer id = allClsIds.get(clsFullName);
if (id == null) {
throw new JadxRuntimeException("Unknown class name: " + clsFullName);
}
return id;
}
private void saveNamesMap() {
@@ -250,6 +259,13 @@ public class DiskCodeCache implements ICodeCache {
String clsName = in.readUTF();
int clsId = in.readInt();
namesMap.put(clsName, clsId);
Integer prevId = allClsIds.get(clsName);
if (prevId == null || prevId != clsId) {
LOG.debug("Unexpected class id, got: {}, expect: {}", clsId, prevId);
LOG.warn("Inconsistent disk cache, resetting...");
reset();
return;
}
}
LOG.info("Found {} classes in disk cache, dir: {}", count, metaDir.getParent());
} catch (Exception e) {
@@ -271,6 +287,16 @@ public class DiskCodeCache implements ICodeCache {
return Paths.get(firstByte, FileUtils.intToHex(clsId) + ext);
}
private Map<String, Integer> buildClassIdsMap(List<ClassNode> classes) {
int clsCount = classes.size();
Map<String, Integer> map = new HashMap<>(clsCount);
for (int i = 0; i < clsCount; i++) {
ClassNode cls = classes.get(i);
map.put(cls.getRawName(), i);
}
return map;
}
@SuppressWarnings("ResultOfMethodCallIgnored")
@Override
public void close() throws IOException {
@@ -0,0 +1,98 @@
package jadx.gui.utils.res;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.jetbrains.annotations.Nullable;
import jadx.api.ICodeInfo;
import jadx.api.ResourceFileContent;
import jadx.api.ResourceType;
import jadx.core.xmlgen.ResContainer;
import jadx.gui.treemodel.JResource;
public class ResTableHelper {
/**
* Build UI tree for resource table container.
*
* @return root nodes
*/
public static List<JResource> buildTree(ResContainer resTable) {
ResTableHelper resTableHelper = new ResTableHelper();
resTableHelper.process(resTable);
return resTableHelper.roots;
}
private final List<JResource> roots = new ArrayList<>();
private final Map<String, JResource> dirs = new HashMap<>();
private ResTableHelper() {
}
private void process(ResContainer resTable) {
for (ResContainer subFile : resTable.getSubFiles()) {
loadSubNodes(subFile);
}
}
private void loadSubNodes(ResContainer rc) {
String resName = rc.getName();
int split = resName.lastIndexOf('/');
String dir;
String name;
if (split == -1) {
dir = null;
name = resName;
} else {
dir = resName.substring(0, split);
name = resName.substring(split + 1);
}
ICodeInfo code = rc.getText();
ResourceFileContent fileContent = new ResourceFileContent(name, ResourceType.XML, code);
JResource resFile = new JResource(fileContent, resName, name, JResource.JResType.FILE);
addResFile(dir, resFile);
for (ResContainer subFile : rc.getSubFiles()) {
loadSubNodes(subFile);
}
}
private void addResFile(@Nullable String dir, JResource resFile) {
if (dir == null) {
roots.add(resFile);
return;
}
JResource dirRes = dirs.get(dir);
if (dirRes != null) {
dirRes.addSubNode(resFile);
return;
}
JResource parentDir = null;
int splitPos = -1;
while (true) {
int prevStart = splitPos + 1;
splitPos = dir.indexOf('/', prevStart);
boolean last = splitPos == -1;
String path = last ? dir : dir.substring(0, splitPos);
JResource curDir = dirs.get(path);
if (curDir == null) {
String dirName = last ? dir.substring(prevStart) : dir.substring(prevStart, splitPos);
curDir = new JResource(null, dirName, JResource.JResType.DIR);
dirs.put(path, curDir);
if (parentDir == null) {
roots.add(curDir);
} else {
parentDir.addSubNode(curDir);
}
}
if (last) {
curDir.addSubNode(resFile);
return;
}
parentDir = curDir;
}
}
}
@@ -0,0 +1,29 @@
package jadx.gui.utils.rx;
import java.util.concurrent.atomic.AtomicBoolean;
import io.reactivex.disposables.Disposable;
public class CustomDisposable implements Disposable {
private final AtomicBoolean disposed = new AtomicBoolean(false);
private final Runnable disposeTask;
public CustomDisposable(Runnable disposeTask) {
this.disposeTask = disposeTask;
}
@Override
public void dispose() {
try {
disposeTask.run();
} finally {
disposed.set(true);
}
}
@Override
public boolean isDisposed() {
return disposed.get();
}
}
@@ -0,0 +1,43 @@
package jadx.gui.utils.rx;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import javax.swing.JTextField;
import javax.swing.event.DocumentListener;
import io.reactivex.BackpressureStrategy;
import io.reactivex.Flowable;
import io.reactivex.FlowableOnSubscribe;
import jadx.gui.utils.ui.DocumentUpdateListener;
public class RxUtils {
public static Flowable<String> textFieldChanges(final JTextField textField) {
FlowableOnSubscribe<String> source = emitter -> {
DocumentListener listener = new DocumentUpdateListener(ev -> emitter.onNext(textField.getText()));
textField.getDocument().addDocumentListener(listener);
emitter.setDisposable(new CustomDisposable(() -> textField.getDocument().removeDocumentListener(listener)));
};
return Flowable.create(source, BackpressureStrategy.LATEST).distinctUntilChanged();
}
public static Flowable<String> textFieldEnterPress(final JTextField textField) {
FlowableOnSubscribe<String> source = emitter -> {
KeyListener keyListener = new KeyAdapter() {
@Override
public void keyPressed(KeyEvent ev) {
if (ev.getKeyCode() == KeyEvent.VK_ENTER) {
emitter.onNext(textField.getText());
}
}
};
textField.addKeyListener(keyListener);
emitter.setDisposable(new CustomDisposable(() -> textField.removeKeyListener(keyListener)));
};
return Flowable.create(source, BackpressureStrategy.LATEST).distinctUntilChanged();
}
}
@@ -0,0 +1,121 @@
package jadx.gui.utils.tools;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.List;
import java.util.stream.Stream;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Automatically add new i18n lines from reference (EN) into others languages
*/
public class NLSAddNewLines {
private static final Logger LOG = LoggerFactory.getLogger(NLSAddNewLines.class);
private static final Path I18N_PATH = Paths.get("src/main/resources/i18n/");
private static final String GUI_MODULE_DIR = "jadx-gui";
public static void main(String[] args) {
try {
process();
} catch (Exception e) {
LOG.error("Failed to add new i18n lines", e);
}
}
private static void process() throws Exception {
String reference = "Messages_en_US.properties";
Path refPath = getRefPath(reference);
List<String> refLines = Files.readAllLines(refPath);
try (Stream<Path> pathStream = Files.list(refPath.toAbsolutePath().getParent())) {
pathStream.forEach(path -> {
if (path.getFileName().equals(refPath.getFileName())) {
return;
}
try {
applyFix(refLines, path);
} catch (Exception e) {
throw new RuntimeException(e);
}
});
}
}
private static void applyFix(List<String> refLines, Path path) throws IOException {
List<String> lines = Files.readAllLines(path);
int linesCount = lines.size();
if (refLines.size() <= linesCount) {
LOG.info("Skip {}, already fixed", path);
return;
}
boolean updated = false;
for (int i = 0; i < linesCount; i++) {
String line = lines.get(i);
String refLine = refLines.get(i);
if (!isSameKey(refLine, line)) {
if (refLine.isEmpty()) {
lines.add(i, "");
} else {
lines.add(i, "#" + refLine);
}
updated = true;
}
}
if (updated) {
LOG.info("Updating {}", path);
Files.write(path, lines, StandardCharsets.UTF_8);
}
}
private static boolean isSameKey(String refLine, String line) {
int refLen = refLine.length();
int len = line.length();
if (refLen == 0) {
return len == 0;
}
if (len == 0) {
return false;
}
int pos = 0;
// skip comment and spaces
while (pos < len) {
char ch = line.charAt(pos);
if (ch == '#' || ch == ' ') {
pos++;
} else {
break;
}
}
int refPos = 0;
while (true) {
char refCh = refLine.charAt(refPos);
if (refCh == ' ' || refCh == '=') {
return true;
}
char ch = line.charAt(pos);
if (refCh != ch) {
return false;
}
refPos++;
pos++;
}
}
private static Path getRefPath(String reference) {
Path path = I18N_PATH.resolve(reference);
if (Files.exists(path)) {
return path;
}
Path rootPath = Paths.get(GUI_MODULE_DIR).resolve(I18N_PATH).resolve(reference);
if (Files.exists(rootPath)) {
return rootPath;
}
throw new RuntimeException("Can't find reference I18N: " + reference);
}
}
@@ -11,6 +11,10 @@ public class ActionHandler extends AbstractAction {
private final Consumer<ActionEvent> consumer;
public ActionHandler(Runnable action) {
this.consumer = ev -> action.run();
}
public ActionHandler(Consumer<ActionEvent> consumer) {
this.consumer = consumer;
}
@@ -4,7 +4,7 @@ menu.file=Datei
menu.view=Ansicht
menu.recent_projects=Aktuelle Projekte
menu.no_recent_projects=Keine aktuellen Projekte
menu.preferences=Präferenzen
menu.preferences=Einstellungen
menu.sync=Mit Editor synchronisieren
menu.flatten=Codepaket erweitern
menu.heapUsageBar=Speicherverbrauchsleiste anzeigen
@@ -23,26 +23,31 @@ menu.update_label=Neue Version %s verfügbar!
file.open_action=Datei öffnen…
file.add_files_action=Dateien hinzufügen…
file.open_title=Datei öffnen
#file.open_project=Open project
file.new_project=Neues Projekt
file.save_project=Projekt speichern
file.save_project_as=Projekt speichern als…
#file.reload=Reload files
#file.live_reload=Live reload
#file.live_reload_desc=Auto reload files on changes
#file.export_mappings_as=
file.reload=Dateien neu laden
file.live_reload=Live nachladen
file.live_reload_desc=Dateien bei Änderungen autom. neuladen
file.export_mappings_as=Zuordnungen exportieren als…
file.save_all=Alles speichern
file.export_gradle=Als Gradle-Projekt speichern
file.save_all_msg=Verzeichnis für das Speichern dekompilierter Ressourcen auswählen
file.exit=Beenden
#start_page.title=Start page
#start_page.start=Start
#start_page.recent=Recent projects
tree.sources_title=Quelltexte
tree.resources_title=Ressourcen
tree.loading=Laden…
progress.load=Laden
#progress.export_mappings=
progress.export_mappings=Zuordnungen exportieren
progress.decompile=Dekompilieren
#progress.canceling=Canceling
progress.canceling=Breche ab
error_dialog.title=Fehler
@@ -69,7 +74,7 @@ message.userCancelTask=Aufgabe wurde vom Benutzer abgebrochen.
message.memoryLow=Jadx hat zu wenig Speicherplatz. Bitte mit erhöhter maximaler Heap-Größe erneut starten.
message.taskError=Die Aufgabe ist durch Fehler fehlgeschlagen (siehe Protokoll für Details).
message.errorTitle=Fehler
message.load_errors=Laden fehlgeschlagen.\nAnzahl der Fehler: %d\nKlicke auf OK, um die Log-Ansicht zu öffnen"
message.load_errors=Laden fehlgeschlagen.\nAnzahl der Fehler: %d\nKlicke auf OK, um die Log-Ansicht zu öffnen.
message.no_classes=Keine Klassen geladen, nichts zu dekompilieren!
message.saveIncomplete=<html>Speichern unvollständig.<br> %s<br> %d Klassen oder Ressourcen wurden nicht gespeichert!</html>
@@ -84,13 +89,16 @@ common_dialog.add=Hinzufügen
common_dialog.update=Aktualisieren
common_dialog.remove=Entfernen
#file_dialog.supported_files=Supported files
#file_dialog.load_dir_title=Load directory
#file_dialog.load_dir_confirm=Load all files from directory?
file_dialog.supported_files=Unterstützte Dateien
file_dialog.load_dir_title=Verzeichnis laden
file_dialog.load_dir_confirm=Alle Dateien aus dem Verzeichnis laden?
search_dialog.open=Öffnen
search_dialog.cancel=Beenden
search_dialog.open_by_name=Nach Text suchen:
#search_dialog.search_button=Search
#search_dialog.search_history=Search history
#search_dialog.auto_search=Auto search
search_dialog.search_in=Suche in Definitionen von:
search_dialog.class=Klassen
search_dialog.method=Methoden
@@ -98,12 +106,14 @@ search_dialog.field=Felder
search_dialog.code=Code
search_dialog.options=Suchoptionen:
search_dialog.ignorecase=Groß/Kleinschreibung ignorieren
#search_dialog.load_more=Load more
#search_dialog.load_all=Load all
#search_dialog.results_incomplete=Found %d+
#search_dialog.results_complete=Found %d (complete)
search_dialog.load_more=Mehr laden
search_dialog.load_all=Alle laden
#search_dialog.stop=Stop
search_dialog.results_incomplete=%d+ gefunden
search_dialog.results_complete=%d gefunden (komplett)
search_dialog.col_node=Knoten
search_dialog.col_code=Code
#search_dialog.sort_results=Sort results
search_dialog.regex=Regex
search_dialog.active_tab=Nur aktiver Tab
search_dialog.comments=Kommentare
@@ -128,28 +138,29 @@ preferences.title=Einstellungen
preferences.deobfuscation=Deobfuskierung
preferences.appearance=Aussehen
preferences.decompile=Dekompilierung
#preferences.plugins=Plugins
preferences.plugins=Plugins
preferences.project=Projekt
preferences.other=Andere
preferences.language=Sprache
preferences.lineNumbersMode=Editor Zeilennummern-Modus
#preferences.jumpOnDoubleClick=Enable jump on double click
preferences.jumpOnDoubleClick=Sprung bei Doppelklick aktivieren
preferences.check_for_updates=Nach Updates beim Start suchen
#preferences.useDx=Use dx/d8 to convert java bytecode
#preferences.decompilationMode=Decompilation mode
#preferences.codeCacheMode=Code cache mode
preferences.useDx=dx/d8 zur Konvertierung von Java Bytecode verwenden
preferences.decompilationMode=Dekompilierungsmodus
preferences.codeCacheMode=Cache-Code-Modus
preferences.showInconsistentCode=Inkonsistenten Code anzeigen
preferences.escapeUnicode=Unicodezeichen escapen
preferences.replaceConsts=Konstanten ersetzen
preferences.respectBytecodeAccessModifiers=Modifikatoren für Bytecode-Zugriff beachten
preferences.useImports=Import statements generieren
#preferences.useDebugInfo=Use debug info
preferences.useDebugInfo=Debug-Infos verwenden
preferences.inlineAnonymous=Anonyme Inline-Klassen
preferences.inlineMethods=Inline-Methoden
#preferences.extractFinally=Extract finally block
preferences.fsCaseSensitive=Dateisystem unterscheidet zwischen Groß/Kleinschreibung
preferences.skipResourcesDecode=Keine Ressourcen dekodieren
#preferences.useKotlinMethodsForVarNames=Use kotlin methods for variables rename
#preferences.commentsLevel=Code comments level
preferences.useKotlinMethodsForVarNames=Kotlin-Methoden für die Umbenennung von Variablen verwenden
preferences.commentsLevel=Level der Code Kommentierungen
preferences.autoSave=Autom. speichern
preferences.threads=Verarbeitungs-Thread-Anzahl
preferences.excludedPackages=Ausgeschlossene Pakete
@@ -166,11 +177,12 @@ preferences.start_jobs=Autom. Hintergrunddekompilierung starten
preferences.select_font=Ändern
preferences.select_smali_font=Ändern
preferences.deobfuscation_on=Deobfuskierung aktivieren
#preferences.deobfuscation_map_file_mode=Map file handle mode
preferences.deobfuscation_map_file_mode=Umgang mit Map-Dateien
preferences.deobfuscation_min_len=Minimale Namenlänge
preferences.deobfuscation_max_len=Maximale Namenlänge
preferences.deobfuscation_source_alias=Quelldateiname als Klassennamen-Alias verwenden
preferences.deobfuscation_kotlin_metadata=Kotlin-Metadaten nach Klassen- und Paketnamen analysieren
#preferences.deobfuscation_res_name_source=Better resources name source
preferences.save=Speichern
preferences.cancel=Abbrechen
preferences.reset=Zurücksetzen
@@ -213,7 +225,7 @@ popup.exclude_packages=Pakete ausschließen
popup.add_comment=Kommentar
popup.search_comment=Kommentar suchen
popup.rename=Umbennen
popup.search=Search "%s"
popup.search=Suche "%s"
popup.search_global=Globale Suche "%s"
exclude_dialog.title=Paketauswahl
@@ -23,6 +23,7 @@ menu.update_label=New version %s available!
file.open_action=Open files ...
file.add_files_action=Add files
file.open_title=Open file
file.open_project=Open project
file.new_project=New project
file.save_project=Save project
file.save_project_as=Save project as...
@@ -35,6 +36,10 @@ file.export_gradle=Save as gradle project
file.save_all_msg=Select directory for save decompiled sources
file.exit=Exit
start_page.title=Start page
start_page.start=Start
start_page.recent=Recent projects
tree.sources_title=Source code
tree.resources_title=Resources
tree.loading=Loading...
@@ -69,7 +74,7 @@ message.userCancelTask=Task was canceled by user.
message.memoryLow=Jadx is running low on memory. Please restart with increased maximum heap size.
message.taskError=Task failed with error (check log for details).
message.errorTitle=Error
message.load_errors=Load failed.\nErrors count: %d\nClick OK to open log viewer"
message.load_errors=Load failed.\nErrors count: %d\nClick OK to open log viewer.
message.no_classes=No classes loaded, nothing to decompile!
message.saveIncomplete=<html>Save incomplete.<br> %s<br> %d classes or resources were not saved!</html>
@@ -91,19 +96,24 @@ file_dialog.load_dir_confirm=Load all files from directory?
search_dialog.open=Open
search_dialog.cancel=Cancel
search_dialog.open_by_name=Search for text:
search_dialog.search_button=Search
search_dialog.search_history=Search history
search_dialog.auto_search=Auto search
search_dialog.search_in=Search definitions of:
search_dialog.class=Class
search_dialog.method=Method
search_dialog.field=Field
search_dialog.code=Code
search_dialog.options=Search options:
search_dialog.ignorecase=Case insensitive
search_dialog.ignorecase=Case-insensitive
search_dialog.load_more=Load more
search_dialog.load_all=Load all
search_dialog.stop=Stop
search_dialog.results_incomplete=Found %d+
search_dialog.results_complete=Found %d (complete)
search_dialog.col_node=Node
search_dialog.col_code=Code
search_dialog.sort_results=Sort results
search_dialog.regex=Regex
search_dialog.active_tab=Active tab only
search_dialog.comments=Comments
@@ -146,7 +156,8 @@ preferences.useImports=Use import statements
preferences.useDebugInfo=Use debug info
preferences.inlineAnonymous=Inline anonymous classes
preferences.inlineMethods=Inline methods
preferences.fsCaseSensitive=File system is case sensitive
preferences.extractFinally=Extract finally block
preferences.fsCaseSensitive=File system is case-sensitive
preferences.skipResourcesDecode=Don't decode resources
preferences.useKotlinMethodsForVarNames=Use kotlin methods for variables rename
preferences.commentsLevel=Code comments level
@@ -171,6 +182,7 @@ preferences.deobfuscation_min_len=Minimum name length
preferences.deobfuscation_max_len=Maximum name length
preferences.deobfuscation_source_alias=Use source file name as class name alias
preferences.deobfuscation_kotlin_metadata=Parse Kotlin metadata for class and package names
preferences.deobfuscation_res_name_source=Better resources name source
preferences.save=Save
preferences.cancel=Cancel
preferences.reset=Reset
@@ -298,7 +310,7 @@ adb_dialog.disconnected=ADB server disconnected.
adb_dialog.start_okay=ADB server started on port: %s.
adb_dialog.start_fail=Failed to start ADB server on port: %s!
adb_dialog.forward_fail=Failed to forward for some reasons.
adb_dialog.being_debugged_msg=This process seems like it's being debugged, should we proceed?"
adb_dialog.being_debugged_msg=This process seems like it's being debugged, should we proceed?
adb_dialog.unknown_android_ver=Failed to get Android release version, use Android 8 as default?
adb_dialog.being_debugged_title=It's Debugging by other.
adb_dialog.init_dbg_fail=Failed to init debugger.
@@ -23,6 +23,7 @@ menu.update_label=¡Nueva versión %s disponible!
file.open_action=Abrir archivo...
#file.add_files_action=Add files ...
file.open_title=Abrir archivo
#file.open_project=Open project
#file.new_project=
#file.save_project=
#file.save_project_as=
@@ -35,6 +36,10 @@ file.export_gradle=Guardar como proyecto Gradle
file.save_all_msg=Seleccionar carpeta para guardar fuentes descompiladas
file.exit=Salir
#start_page.title=Start page
#start_page.start=Start
#start_page.recent=Recent projects
tree.sources_title=Código fuente
tree.resources_title=Recursos
tree.loading=Cargando...
@@ -69,7 +74,7 @@ nav.forward=Adelante
#message.memoryLow=Jadx is running low on memory. Please restart with increased maximum heap size.
#message.taskError=Task failed with error (check log for details).
#message.errorTitle=Error
#message.load_errors=Load failed.\nErrors count: %d\nClick OK to open log viewer"
#message.load_errors=Load failed.\nErrors count: %d\nClick OK to open log viewer.
#message.no_classes=No classes loaded, nothing to decompile!
#message.saveIncomplete=<html>Save incomplete.<br> %s<br> %d classes or resources were not saved!</html>
@@ -91,6 +96,9 @@ nav.forward=Adelante
search_dialog.open=Abrir
search_dialog.cancel=Cancelar
search_dialog.open_by_name=Buscar texto:
#search_dialog.search_button=Search
#search_dialog.search_history=Search history
#search_dialog.auto_search=Auto search
search_dialog.search_in=Buscar definiciones de:
search_dialog.class=Clase
search_dialog.method=Método
@@ -100,10 +108,12 @@ search_dialog.options=Opciones de búsqueda:
search_dialog.ignorecase=Ignorar minúsculas/mayúsculas
#search_dialog.load_more=Load more
#search_dialog.load_all=Load all
#search_dialog.stop=Stop
#search_dialog.results_incomplete=Found %d+
#search_dialog.results_complete=Found %d (complete)
search_dialog.col_node=Nodo
search_dialog.col_code=Código
#search_dialog.sort_results=Sort results
search_dialog.regex=Regex
#search_dialog.active_tab=Active tab only
#search_dialog.comments=Comments
@@ -146,6 +156,7 @@ preferences.replaceConsts=Reemplazar constantes
#preferences.useDebugInfo=Use debug info
#preferences.inlineAnonymous=
#preferences.inlineMethods=Inline methods
#preferences.extractFinally=Extract finally block
#preferences.fsCaseSensitive=
preferences.skipResourcesDecode=No descodificar recursos
#preferences.useKotlinMethodsForVarNames=Use kotlin methods for variables rename
@@ -171,6 +182,7 @@ preferences.deobfuscation_min_len=Longitud mínima del nombre
preferences.deobfuscation_max_len=Longitud máxima del nombre
preferences.deobfuscation_source_alias=Usar el nombre del source como alias para la clase
preferences.deobfuscation_kotlin_metadata=Parse Kotlin metadatos para nombres de clase y paquete
#preferences.deobfuscation_res_name_source=Better resources name source
preferences.save=Guardar
preferences.cancel=Cancelar
preferences.reset=Reestablecer
@@ -298,7 +310,7 @@ certificate.serialPubKeyY=Y
#adb_dialog.start_okay=ADB server started on port: %s.
#adb_dialog.start_fail=Failed to start ADB server on port: %s!
#adb_dialog.forward_fail=Failed to forward for some reasons.
#adb_dialog.being_debugged_msg=This process seems like it's being debugged, should we proceed?"
#adb_dialog.being_debugged_msg=This process seems like it's being debugged, should we proceed?
#adb_dialog.unknown_android_ver=Failed to get Android release version, use Android 8 as default?
#adb_dialog.being_debugged_title=It's Debugging by other.
#adb_dialog.init_dbg_fail=Failed to init debugger.
@@ -23,6 +23,7 @@ menu.update_label=새 버전 %s 이(가) 존재합니다!
file.open_action=파일 열기 ...
file.add_files_action=파일 추가
file.open_title=파일 열기
#file.open_project=Open project
file.new_project=새 프로젝트
file.save_project=프로젝트 저장
file.save_project_as=다른 이름으로 프로젝트 저장...
@@ -35,6 +36,10 @@ file.export_gradle=Gradle 프로젝트로 저장
file.save_all_msg=디컴파일된 소스를 저장할 디렉토리 선택
file.exit=나가기
#start_page.title=Start page
#start_page.start=Start
#start_page.recent=Recent projects
tree.sources_title=소스코드
tree.resources_title=리소스
tree.loading=로딩중...
@@ -69,7 +74,7 @@ message.userCancelTask=사용자가 작업을 취소했습니다.
message.memoryLow=Jadx의 메모리가 부족합니다. 최대 힙 크기를 늘린 후 다시 시작하십시오.
message.taskError=작업이 오류로 실패했습니다. (자세한 내용은 로그 확인)
message.errorTitle=오류
message.load_errors=로드하지 못했습니다.\n오류 수: %d\n로그 뷰어를 열려면 확인을 클릭하십시오."
message.load_errors=로드하지 못했습니다.\n오류 수: %d\n로그 뷰어를 열려면 확인을 클릭하십시오.
message.no_classes=로드된 클래스 없음, 디컴파일 대상 존재하지 않음
message.saveIncomplete=<html>저장이 완료되지 않았습니다.<br> %s<br> %d개의 클래스 또는 리소스가 저장되지 않았습니다!</html>
@@ -91,6 +96,9 @@ common_dialog.remove=삭제
search_dialog.open=열기
search_dialog.cancel=취소
search_dialog.open_by_name=텍스트 검색 :
#search_dialog.search_button=Search
#search_dialog.search_history=Search history
#search_dialog.auto_search=Auto search
search_dialog.search_in=정의 검색 :
search_dialog.class=클래스
search_dialog.method=메소드
@@ -100,10 +108,12 @@ search_dialog.options=옵션 검색:
search_dialog.ignorecase=대소문자 구분 안함
#search_dialog.load_more=Load more
#search_dialog.load_all=Load all
#search_dialog.stop=Stop
#search_dialog.results_incomplete=Found %d+
#search_dialog.results_complete=Found %d (complete)
search_dialog.col_node=노드
search_dialog.col_code=코드
#search_dialog.sort_results=Sort results
search_dialog.regex=정규식
search_dialog.active_tab=열려 있는 탭에서만 검색
search_dialog.comments=주석
@@ -146,6 +156,7 @@ preferences.useImports=import 문 사용
#preferences.useDebugInfo=Use debug info
preferences.inlineAnonymous=인라인 익명 클래스
preferences.inlineMethods=인라인 메서드
#preferences.extractFinally=Extract finally block
preferences.fsCaseSensitive=파일 시스템 대소문자 구별
preferences.skipResourcesDecode=리소스 디코딩 하지 않기
#preferences.useKotlinMethodsForVarNames=Use kotlin methods for variables rename
@@ -171,6 +182,7 @@ preferences.deobfuscation_min_len=최소 이름 길이
preferences.deobfuscation_max_len=최대 이름 길이
preferences.deobfuscation_source_alias=소스 파일 이름을 클래스 이름 별칭으로 사용
preferences.deobfuscation_kotlin_metadata=클래스 및 패키지 이름에 대한 Kotlin 메타 데이터 파싱
#preferences.deobfuscation_res_name_source=Better resources name source
preferences.save=저장
preferences.cancel=취소
preferences.reset=재설정
@@ -0,0 +1,321 @@
language.name=Português
menu.file=Arquivo
menu.view=Ver
menu.recent_projects=Projetos recentes
menu.no_recent_projects=Nenhum projeto recente
menu.preferences=Preferências
menu.sync=Sincronizar com editor
menu.flatten=Mostrar pacotes achatados
menu.heapUsageBar=Mostrar uso de memória
menu.alwaysSelectOpened=Sempre selecionar arquivo/classe aberta
menu.navigation=Navegação
menu.text_search=Buscar por texto
menu.class_search=Buscar por classe
menu.comment_search=Busca por comentário
menu.tools=Ferramentas
menu.deobfuscation=Desofuscar
menu.log=Visualizador de log
menu.help=Ajuda
menu.about=Sobre
menu.update_label=Nova versão %s disponível!
file.open_action=Abrir arquivos...
file.add_files_action=Adicionar arquivos
file.open_title=Abrir arquivo
#file.open_project=Open project
file.new_project=Novo projeto
file.save_project=Salvar projeto
file.save_project_as=Salvar projeto como...
file.reload=Recarregar arquivos
file.live_reload=Recarregar em tempo real
file.live_reload_desc=Recarregar arquivos automaticamente ao serem alterados
file.export_mappings_as=Exportar mappings como...
file.save_all=Salvar tudo
file.export_gradle=Salvar como um projeto gradle
file.save_all_msg=Selecionar diretório para salvar arquivos descompilados
file.exit=Sair
#start_page.title=Start page
#start_page.start=Start
#start_page.recent=Recent projects
tree.sources_title=Código fonte
tree.resources_title=Recursos
tree.loading=Carregando...
progress.load=Carregando
progress.export_mappings=Exportando mappings
progress.decompile=Descompilando
progress.canceling=Cancelando
error_dialog.title=Erro
search.previous=Anterior
search.next=Próximo
search.mark_all=Marcar todos
search.regex=Regex
search.match_case=Match Case
search.whole_word=Palavra inteira
search.find=Encontrar
tabs.copy_class_name=Copiar nome
tabs.close=Fechar
tabs.closeOthers=Fechar outros
tabs.closeAll=Fechar todos
tabs.code=Código
tabs.smali=Smali
nav.back=Voltar
nav.forward=Avançar
message.taskTimeout=Tarefa excedeu limite de tempo de %d ms.
message.userCancelTask=Tarefa foi cancelada pelo usuário.
message.memoryLow=Jadx está rodando com pouca memória. Por favor reinicie com um limite de memória heap maior.
message.taskError=Tarefa falhou com erro (cheque o log para detalhes).
message.errorTitle=Erro
message.load_errors=Carregamento falhou.\nNúmero de erros: %d\nClique em ok para abrir o visualizador de log.
message.no_classes=Nenhuma classe carregada, nada para ser descompilado!
message.saveIncomplete=<html>Operação não foi completa.<br> %s<br> %d classes ou recursos não foram salvos!</html>
message.indexIncomplete=<html>Indexação de algumas classes foram ignoradas.<br> %s<br> %d classes não foram indexadas não vão aparecer nos resultados de busca!</html>
message.indexingClassesSkipped=<html>Jadx está rodando com pouca memória. Por conta disso, %d classes não foram indexadas.<br>Se você deseja que todas classes sejam indexadas, reinicie com um limite de memória heap maior.</html>
heapUsage.text=Uso de memória do JADX: %.2f GB of %.2f GB
common_dialog.ok=Ok
common_dialog.cancel=Cancelar
common_dialog.add=Adicionar
common_dialog.update=Atualizar
common_dialog.remove=Remover
file_dialog.supported_files=Arquivos suportados
file_dialog.load_dir_title=Carregar diretório
file_dialog.load_dir_confirm=Carregar todos arquivos do diretório?
search_dialog.open=Abrir
search_dialog.cancel=Cancelar
search_dialog.open_by_name=Buscar por text:
#search_dialog.search_button=Search
#search_dialog.search_history=Search history
#search_dialog.auto_search=Auto search
search_dialog.search_in=Buscar definições de:
search_dialog.class=Classe
search_dialog.method=Método
search_dialog.field=Propriedade
search_dialog.code=Código
search_dialog.options=Opções de busca:
search_dialog.ignorecase=Não diferencia maiúsculas de minúsculas
search_dialog.load_more=Carregar mais
search_dialog.load_all=Carregar todas
#search_dialog.stop=Stop
search_dialog.results_incomplete=Encontradas %d+
search_dialog.results_complete=Encontradas %d (complete)
search_dialog.col_node=
search_dialog.col_code=Código
#search_dialog.sort_results=Sort results
search_dialog.regex=Regex
search_dialog.active_tab=Apenas abas ativas
search_dialog.comments=Comentários
search_dialog.resource=Recursos
search_dialog.keep_open=Manter aberto
search_dialog.tip_searching=Buscando
usage_dialog.title=Busca por utilização
usage_dialog.label=Usado por:
comment_dialog.title.add=Adicionar comentário ao código
comment_dialog.title.update=Atualizar comentário do código
comment_dialog.label=Comentário:
comment_dialog.usage=Use Shift + Enter para pular uma linha
log_viewer.title=Visualizador de log
log_viewer.log_level=Nível do log:
about_dialog.title=Sobre o JADX
preferences.title=Preferências
preferences.deobfuscation=Desofuscar
preferences.appearance=Aparência
preferences.decompile=Descompilação
preferences.plugins=Plugins
preferences.project=Projeto
preferences.other=Outro
preferences.language=Idioma
preferences.lineNumbersMode=Modo do contador de linhas do editor
preferences.jumpOnDoubleClick=Ativar salto no duplo clique
preferences.check_for_updates=Verificar por atualizações ao inicializar
preferences.useDx=Usar dx/d8 para converter bytecode Java
preferences.decompilationMode=Modo de descompilação
preferences.codeCacheMode=Modo de cachê do código
preferences.showInconsistentCode=Mostrar código inconsistent
preferences.escapeUnicode=Escapar unicode
preferences.replaceConsts=Substituir constantes
preferences.respectBytecodeAccessModifiers=Respeitar modificadores de acesso do bytecode
preferences.useImports=Utilizar declaração de imports
preferences.useDebugInfo=Utilizar informação de depuração
preferences.inlineAnonymous=Classes anônimas de uma linha
preferences.inlineMethods=Métodos de uma linha
preferences.extractFinally=Extrair blocos finally
preferences.fsCaseSensitive=Sistema de arquivo diferencia maiúsculas de minúsculas
preferences.skipResourcesDecode=Não decodificar recursos
preferences.useKotlinMethodsForVarNames=Usar métodos do kotlin para renomear variáveis
preferences.commentsLevel=Nível de comentários do código
preferences.autoSave=Salvar automaticamente
preferences.threads=Número de threads no processo
preferences.excludedPackages=Pacotes ignorados
preferences.excludedPackages.tooltip=Lista espaço de pacotes que não vão ser descompilados ou indexados (economiza RAM)
preferences.excludedPackages.button=Editar
preferences.excludedPackages.editDialog=<html>Lista espaço de pacotes que não vão ser descompilados ou indexados (economiza RAM)<br>ex: <code>android.support</code></html>
preferences.cfg=Gera gráficos de métodos CFG no formato de pontos ('dot')
preferences.raw_cfg=Gera gráficos CFG no formato RAW
preferences.font=Fonte do editor
preferences.smali_font=Fonte do editor de smali
preferences.laf_theme=Tema
preferences.theme=Tema do editor
preferences.start_jobs=Inicializar descompilação automaticamente em segundo-plano
preferences.select_font=Alterar
preferences.select_smali_font=Alterar
preferences.deobfuscation_on=Ativar desofuscação
preferences.deobfuscation_map_file_mode=Modo do arquivo Map
preferences.deobfuscation_min_len=Tamanho mínimo do nome
preferences.deobfuscation_max_len=Tamanho máximo do nome
preferences.deobfuscation_source_alias=Utilizar nome do arquivo como apelido da classe
preferences.deobfuscation_kotlin_metadata=Parsear metadados do kotlin para nome de classes e pacotes
preferences.deobfuscation_res_name_source=Melhora nome da fonte dos recursos
preferences.save=Salvar
preferences.cancel=Cancelar
preferences.reset=Redefinir
preferences.reset_message=Redefinir configurações para o padrão?
preferences.reset_title=Redefinir configurações
preferences.copy=Copy to clipboard
preferences.copy_message=Todas configurações foram copiadas para área de transferência
preferences.rename=Renomear identificadores
preferences.rename_case=Corrigir problemas de capitalização (case sensitivity)
preferences.rename_valid=Deixá-las válidas
preferences.rename_printable=Deixá-las imprimíveis (printable)
preferences.search_res_title=Buscar recursos
preferences.res_file_ext=Extensões de arquivos (ex: .xml|.html), * significa todas
preferences.res_skip_file=Pular arquivos excedidos
msg.open_file=Abra um arquivo
msg.saving_sources=Salvando recursos
msg.language_changed_title=Idioma alterado
msg.language_changed=Novo idioma será mostrado na próxima inicialização.
msg.project_error_title=Erro
msg.project_error=Projeto não pôde ser carregado
msg.cmd_select_class_error=Falha ao selecionar classe\n%s\nA classe não existe.
msg.cant_add_comment=Não é possível adicionar comentários aqui
popup.bytecode_col=Mostrar Dalvik Bytecode
popup.line_wrap=Quebra de linha
popup.undo=Desfazer
popup.redo=Refazer
popup.cut=Cortar
popup.copy=Copiar
popup.paste=Colar
popup.delete=Apagar
popup.select_all=Selecionar todos
popup.frida=Copiar como snippet frida
popup.xposed=Copiar como snippet xposed
popup.find_usage=Buscar uso
popup.go_to_declaration=Ir para declaração
popup.exclude=Ignorar
popup.exclude_packages=Pacotes ignorados
popup.add_comment=Comentar
popup.search_comment=Buscar comentários
popup.rename=Renomear
popup.search=Buscar "%s"
popup.search_global=Busca global "%s"
exclude_dialog.title=Selecionar pacote
exclude_dialog.ok=OK
exclude_dialog.select_all=Selecionar tudo
exclude_dialog.deselect=Remover seleção
exclude_dialog.invert=Inverter
confirm.save_as_title=Confirmar operação
confirm.save_as_message=%s Já existe.\nVocê deseja substituir?
confirm.not_saved_title=Salvar projeto
confirm.not_saved_message=Salvar projeto atual antes de continuar?
certificate.cert_type=Tipo
certificate.serialSigVer=Versão
certificate.serialNumber=Número de série (serial)
certificate.cert_subject=Remetente
certificate.serialValidFrom=Válido de
certificate.serialValidUntil=Válido até
certificate.serialPubKeyType=Tipo de chave pública
certificate.serialPubKeyExponent=Expoente
certificate.serialPubKeyModulus=Módulo
certificate.serialPubKeyModulusSize=Tamanho do módulo (bits)
certificate.serialSigType=Tipo de assinatura
certificate.serialSigOID=Assinatura OID
certificate.serialMD5=Assinatura digital MD5
certificate.serialSHA1=Assinatura digital SHA-1
certificate.serialSHA256=Assinatura digital SHA-256
certificate.serialPubKeyY=Y
apkSignature.signer=Assinador
apkSignature.verificationSuccess=Verificação de assinatura bem-sucedida
apkSignature.verificationFailed=Verificação de assinatura falhou
apkSignature.signatureSuccess=Assinatura válida de apk v%d encontrada
apkSignature.signatureFailed=Assinatura inválida de apk v%d encontrada
apkSignature.errors=Erros
apkSignature.warnings=Avisos
apkSignature.exception=Verificação do APK falhou
apkSignature.unprotectedEntry=Arquivos que não são protegidos pela assinatura v1. Modificações não autorizadas a essas entradas só podem ser detectadas por uma assinatura de versão 2 ou maior.
issues_panel.label=Problemas:
issues_panel.errors=%d error
issues_panel.warnings=%d avisos
issues_panel.tooltip=Abrir no visualizador de log
debugger.process_selector=Selecionar um processo para depurar
debugger.step_into=Passar para próxima chamada de função (F7)
debugger.step_over=Passar sobre próxima chamada de função (F8)
debugger.step_out=Sair da função atual (Shift + F8)
debugger.run=Executar (F9)
debugger.stop=Parar depurador e matar o aplicativo
debugger.pause=Pausar
debugger.rerun=Reexecutar
debugger.cfm_dialog_title=Sair enquanto depura
debugger.cfm_dialog_msg=Você tem certeza de que deseja terminar o depurador?
debugger.popup_set_value=Definir valor
debugger.popup_change_to_zero=Alterar para 0
debugger.popup_change_to_one=Alterar para 1
debugger.popup_copy_value=Copiar valor
set_value_dialog.label_value=Valor
set_value_dialog.btn_set=Definir Valor
set_value_dialog.title=Definir Valor
set_value_dialog.neg_msg=Falha ao definir valor.
set_value_dialog.sel_type=Selecionar um tipo para definir valor.
adb_dialog.addr=Endereço do ADB
adb_dialog.port=Porta do ADB
adb_dialog.path=Caminho do ADB
adb_dialog.launch_app=Iniciar Aplicativo
adb_dialog.start_server=Iniciar servidor ADB
adb_dialog.refresh=Recarregar
adb_dialog.tip_devices=%d dispositivos
adb_dialog.device_node=dispositivo
adb_dialog.missing_path=Caminho do ADB deve ser especificado para iniciar o servidor.
adb_dialog.waiting=Aguardando para conectar no servidor ADB...
adb_dialog.connecting=Conectando ao servidor ADB, endereço: %s:%s...
adb_dialog.connect_okay=Servidor ADB conectado, endereço: %s:%s
adb_dialog.connect_fail=Falha ao se conectar ao servidor ADB.
adb_dialog.disconnected=Servidor ADB desconectado.
adb_dialog.start_okay=Servidor ADB inicializado na porta: %s.
adb_dialog.start_fail=Falha ao iniciar servidor ADB na porta: %s!
adb_dialog.forward_fail=Falha ao encaminhar por um motivo não especificado.
adb_dialog.being_debugged_msg=Esse processo parece estar sendo depurado, deseja continuar?
adb_dialog.unknown_android_ver=Falha ao obter versão do Android, deseja usar o Android 8 como padrão?
adb_dialog.being_debugged_title=Está sendo depurado por outro processo.
adb_dialog.init_dbg_fail=Falha ao iniciar depurador.
adb_dialog.msg_read_mani_fail=Falha ao decodificar AndroidManifest.xml
adb_dialog.no_devices=Não foi possível encontrar nenhum dispositivo para iniciar o aplicativo.
adb_dialog.restart_while_debugging_title=Reiniciar enquanto depura
adb_dialog.restart_while_debugging_msg=Você está depurando um aplicativo, você tem certeza que deseja reiniciar a sessão?
adb_dialog.starting_debugger=Iniciando depurador...

Some files were not shown because too many files have changed in this diff Show More