diff --git a/NOTICE b/NOTICE index 78d7d2ce3..664344923 100644 --- a/NOTICE +++ b/NOTICE @@ -144,7 +144,8 @@ THE POSSIBILITY OF SUCH DAMAGE. Jadx-gui components =================== -RSyntaxTextArea library licensed under modified BSD license: +RSyntaxTextArea library (https://github.com/bobbylight/RSyntaxTextArea) +licensed under modified BSD license: ******************************************************************************* Copyright (c) 2012, Robert Futrell @@ -174,8 +175,27 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ******************************************************************************* +Concurrent Trees (https://code.google.com/p/concurrent-trees/) +licenced under Apache License 2.0: + +******************************************************************************* +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +******************************************************************************* + + Icons copied from several places: - Eclipse Project (JDT UI) - licensed under EPL v1.0 (http://www.eclipse.org/legal/epl-v10.html) - - famfamfam silk icon set (http://www.famfamfam.com/lab/icons/silk/) - licensed under Creative Commons Attribution 2.5 License (http://creativecommons.org/licenses/by/2.5/) + - famfamfam silk icon set (http://www.famfamfam.com/lab/icons/silk/) - licensed + under Creative Commons Attribution 2.5 License (http://creativecommons.org/licenses/by/2.5/) JFontChooser Component - http://sourceforge.jp/projects/jfontchooser/ diff --git a/jadx-core/src/main/java/jadx/core/dex/nodes/parser/FieldInitAttr.java b/jadx-core/src/main/java/jadx/core/dex/nodes/parser/FieldInitAttr.java index b46dd8bbd..da8d6b543 100644 --- a/jadx-core/src/main/java/jadx/core/dex/nodes/parser/FieldInitAttr.java +++ b/jadx-core/src/main/java/jadx/core/dex/nodes/parser/FieldInitAttr.java @@ -7,7 +7,7 @@ import jadx.core.dex.nodes.MethodNode; public class FieldInitAttr implements IAttribute { - public static FieldInitAttr NULL_VALUE = constValue(null); + public static final FieldInitAttr NULL_VALUE = constValue(null); public enum InitType { CONST, diff --git a/jadx-gui/build.gradle b/jadx-gui/build.gradle index e12675394..0db5c19f7 100644 --- a/jadx-gui/build.gradle +++ b/jadx-gui/build.gradle @@ -5,7 +5,7 @@ mainClassName = 'jadx.gui.JadxGUI' dependencies { compile(project(":jadx-core")) compile(project(":jadx-cli")) - compile 'com.fifesoft:rsyntaxtextarea:2.5.6' + compile 'com.fifesoft:rsyntaxtextarea:2.5.7' compile 'com.google.code.gson:gson:2.3.1' compile files('libs/jfontchooser-1.0.5.jar') compile 'com.googlecode.concurrent-trees:concurrent-trees:2.4.0' diff --git a/jadx-gui/src/main/java/jadx/gui/jobs/BackgroundJob.java b/jadx-gui/src/main/java/jadx/gui/jobs/BackgroundJob.java new file mode 100644 index 000000000..47ada2d47 --- /dev/null +++ b/jadx-gui/src/main/java/jadx/gui/jobs/BackgroundJob.java @@ -0,0 +1,87 @@ +package jadx.gui.jobs; + +import jadx.gui.JadxWrapper; + +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.FutureTask; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public abstract class BackgroundJob { + private static final Logger LOG = LoggerFactory.getLogger(DecompileJob.class); + + protected final JadxWrapper wrapper; + private final ThreadPoolExecutor executor; + private Future future; + + public BackgroundJob(JadxWrapper wrapper, int threadsCount) { + this.wrapper = wrapper; + this.executor = (ThreadPoolExecutor) Executors.newFixedThreadPool(threadsCount); + } + + public synchronized Future process() { + if (future != null) { + return future; + } + ExecutorService shutdownExecutor = Executors.newSingleThreadExecutor(); + FutureTask task = new ShutdownTask(); + shutdownExecutor.execute(task); + shutdownExecutor.shutdown(); + future = task; + return future; + } + + private class ShutdownTask extends FutureTask { + public ShutdownTask() { + super(new Callable() { + @Override + public Boolean call() throws Exception { + runJob(); + executor.shutdown(); + return executor.awaitTermination(5, TimeUnit.MINUTES); + } + }); + } + + @Override + public boolean cancel(boolean mayInterruptIfRunning) { + executor.shutdownNow(); + return super.cancel(mayInterruptIfRunning); + } + } + + protected abstract void runJob(); + + public abstract String getInfoString(); + + protected void addTask(Runnable runnable) { + executor.execute(runnable); + } + + public void processAndWait() { + try { + process().get(); + } catch (Exception e) { + LOG.error("BackgroundJob.processAndWait failed", e); + } + } + + public synchronized boolean isComplete() { + try { + return future != null && future.isDone(); + } catch (Exception e) { + LOG.error("BackgroundJob.isComplete failed", e); + return false; + } + } + + public int getProgress() { + return (int) (executor.getCompletedTaskCount() * 100 / (double) executor.getTaskCount()); + } +} diff --git a/jadx-gui/src/main/java/jadx/gui/jobs/BackgroundWorker.java b/jadx-gui/src/main/java/jadx/gui/jobs/BackgroundWorker.java new file mode 100644 index 000000000..0614e7cfe --- /dev/null +++ b/jadx-gui/src/main/java/jadx/gui/jobs/BackgroundWorker.java @@ -0,0 +1,99 @@ +package jadx.gui.jobs; + +import jadx.gui.ui.ProgressPanel; +import jadx.gui.utils.CacheObject; +import jadx.gui.utils.TextSearchIndex; +import jadx.gui.utils.Utils; + +import javax.swing.SwingUtilities; +import javax.swing.SwingWorker; +import java.util.concurrent.Future; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class BackgroundWorker extends SwingWorker { + private static final Logger LOG = LoggerFactory.getLogger(BackgroundWorker.class); + + private final CacheObject cache; + private final ProgressPanel progressPane; + + public BackgroundWorker(CacheObject cacheObject, ProgressPanel progressPane) { + this.cache = cacheObject; + this.progressPane = progressPane; + } + + public void exec() { + if (isDone()) { + return; + } + SwingUtilities.invokeLater(new Runnable() { + @Override + public void run() { + progressPane.setVisible(true); + } + }); + addPropertyChangeListener(progressPane); + execute(); + } + + public void stop() { + if (isDone()) { + return; + } + LOG.debug("Canceling background jobs ..."); + cancel(false); + } + + @Override + protected Void doInBackground() throws Exception { + try { + System.gc(); + LOG.debug("Memory usage: Before decompile: {}", Utils.memoryInfo()); + runJob(cache.getDecompileJob()); + + LOG.debug("Memory usage: Before index: {}", Utils.memoryInfo()); + runJob(cache.getIndexJob()); + LOG.debug("Memory usage: After index: {}", Utils.memoryInfo()); + + System.gc(); + LOG.debug("Memory usage: After gc: {}", Utils.memoryInfo()); + + TextSearchIndex searchIndex = cache.getTextIndex(); + if (cache.getIndexJob().isUseFastSearch() + && searchIndex != null + && searchIndex.getSkippedCount() > 0) { + LOG.warn("Indexing of some classes skipped, count: {}, low memory: {}", + searchIndex.getSkippedCount(), Utils.memoryInfo()); + } + } catch (Exception e) { + LOG.error("Exception in background worker", e); + } + return null; + } + + private void runJob(BackgroundJob job) { + if (isCancelled()) { + return; + } + progressPane.changeLabel(this, job.getInfoString()); + Future future = job.process(); + while (!future.isDone()) { + try { + setProgress(job.getProgress()); + if (isCancelled()) { + future.cancel(false); + } + Thread.sleep(500); + } catch (Exception e) { + LOG.error("Background worker error", e); + } + } + } + + @Override + protected void done() { + progressPane.setVisible(false); + } + +} diff --git a/jadx-gui/src/main/java/jadx/gui/jobs/DecompileJob.java b/jadx-gui/src/main/java/jadx/gui/jobs/DecompileJob.java new file mode 100644 index 000000000..60c263e55 --- /dev/null +++ b/jadx-gui/src/main/java/jadx/gui/jobs/DecompileJob.java @@ -0,0 +1,28 @@ +package jadx.gui.jobs; + +import jadx.api.JavaClass; +import jadx.gui.JadxWrapper; + +public class DecompileJob extends BackgroundJob { + + public DecompileJob(JadxWrapper wrapper, int threadsCount) { + super(wrapper, threadsCount); + } + + protected void runJob() { + for (final JavaClass cls : wrapper.getClasses()) { + addTask(new Runnable() { + @Override + public void run() { + cls.decompile(); + } + }); + } + } + + @Override + public String getInfoString() { + return "Decompiling: "; + } + +} diff --git a/jadx-gui/src/main/java/jadx/gui/jobs/IndexJob.java b/jadx-gui/src/main/java/jadx/gui/jobs/IndexJob.java new file mode 100644 index 000000000..6039f9a21 --- /dev/null +++ b/jadx-gui/src/main/java/jadx/gui/jobs/IndexJob.java @@ -0,0 +1,76 @@ +package jadx.gui.jobs; + +import jadx.api.JavaClass; +import jadx.core.codegen.CodeWriter; +import jadx.gui.JadxWrapper; +import jadx.gui.settings.JadxSettings; +import jadx.gui.utils.CacheObject; +import jadx.gui.utils.CodeLinesInfo; +import jadx.gui.utils.CodeUsageInfo; +import jadx.gui.utils.TextSearchIndex; +import jadx.gui.utils.Utils; + +import org.jetbrains.annotations.NotNull; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class IndexJob extends BackgroundJob { + + private static final Logger LOG = LoggerFactory.getLogger(IndexJob.class); + private final CacheObject cache; + private final boolean useFastSearch; + + public IndexJob(JadxWrapper wrapper, JadxSettings settings, CacheObject cache) { + super(wrapper, settings.getThreadsCount()); + this.useFastSearch = settings.isUseFastSearch(); + this.cache = cache; + } + + protected void runJob() { + final TextSearchIndex index = new TextSearchIndex(); + final CodeUsageInfo usageInfo = new CodeUsageInfo(); + cache.setTextIndex(index); + cache.setUsageInfo(usageInfo); + for (final JavaClass cls : wrapper.getClasses()) { + addTask(new Runnable() { + @Override + public void run() { + try { + index.indexNames(cls); + + CodeLinesInfo linesInfo = new CodeLinesInfo(cls); + String[] lines = splitIntoLines(cls); + + usageInfo.processClass(cls, linesInfo, lines); + if (useFastSearch && Utils.isFreeMemoryAvailable()) { + index.indexCode(cls, linesInfo, lines); + } else { + index.classCodeIndexSkipped(cls); + } + } catch (Exception e) { + LOG.error("Index error in class: {}", cls.getFullName(), e); + } + } + }); + } + } + + @NotNull + protected String[] splitIntoLines(JavaClass cls) { + String[] lines = cls.getCode().split(CodeWriter.NL); + int count = lines.length; + for (int i = 0; i < count; i++) { + lines[i] = lines[i].trim(); + } + return lines; + } + + @Override + public String getInfoString() { + return "Indexing: "; + } + + public boolean isUseFastSearch() { + return useFastSearch; + } +} diff --git a/jadx-gui/src/main/java/jadx/gui/settings/JadxSettings.java b/jadx-gui/src/main/java/jadx/gui/settings/JadxSettings.java index 8b13d6434..1986fa1ef 100644 --- a/jadx-gui/src/main/java/jadx/gui/settings/JadxSettings.java +++ b/jadx-gui/src/main/java/jadx/gui/settings/JadxSettings.java @@ -2,7 +2,6 @@ package jadx.gui.settings; import jadx.cli.JadxCLIArgs; -import javax.swing.JLabel; import java.awt.Font; import java.util.ArrayList; import java.util.Arrays; @@ -10,12 +9,14 @@ import java.util.HashSet; import java.util.List; import java.util.Set; +import org.fife.ui.rsyntaxtextarea.RSyntaxTextArea; + public class JadxSettings extends JadxCLIArgs { private static final String USER_HOME = System.getProperty("user.home"); private static final int RECENT_FILES_COUNT = 15; - private static final Font DEFAULT_FONT = new JLabel().getFont(); + private static final Font DEFAULT_FONT = new RSyntaxTextArea().getFont(); static final Set SKIP_FIELDS = new HashSet(Arrays.asList( "files", "input", "outputDir", "verbose", "printHelp" @@ -27,6 +28,7 @@ public class JadxSettings extends JadxCLIArgs { private boolean checkForUpdates = true; private List recentFiles = new ArrayList(); private String fontStr = ""; + private boolean useFastSearch = false; public void sync() { JadxSettingsAdapter.store(this); @@ -73,10 +75,8 @@ public class JadxSettings extends JadxCLIArgs { } public void addRecentFile(String filePath) { - if (recentFiles.contains(filePath)) { - return; - } - recentFiles.add(filePath); + recentFiles.remove(filePath); + recentFiles.add(0, filePath); int count = recentFiles.size(); if (count > RECENT_FILES_COUNT) { recentFiles.subList(0, count - RECENT_FILES_COUNT).clear(); @@ -136,6 +136,14 @@ public class JadxSettings extends JadxCLIArgs { this.deobfuscationUseSourceNameAsAlias = useSourceNameAsAlias; } + public boolean isUseFastSearch() { + return useFastSearch; + } + + public void setUseFastSearch(boolean useFastSearch) { + this.useFastSearch = useFastSearch; + } + public Font getFont() { if (fontStr.isEmpty()) { return DEFAULT_FONT; diff --git a/jadx-gui/src/main/java/jadx/gui/settings/JadxSettingsAdapter.java b/jadx-gui/src/main/java/jadx/gui/settings/JadxSettingsAdapter.java index 712eb7906..c9c8e08e5 100644 --- a/jadx-gui/src/main/java/jadx/gui/settings/JadxSettingsAdapter.java +++ b/jadx-gui/src/main/java/jadx/gui/settings/JadxSettingsAdapter.java @@ -2,6 +2,7 @@ package jadx.gui.settings; import jadx.gui.JadxGUI; +import java.lang.reflect.Modifier; import java.lang.reflect.Type; import java.util.prefs.Preferences; @@ -25,7 +26,9 @@ public class JadxSettingsAdapter { private static ExclusionStrategy EXCLUDE_FIELDS = new ExclusionStrategy() { @Override public boolean shouldSkipField(FieldAttributes f) { - return JadxSettings.SKIP_FIELDS.contains(f.getName()); + return JadxSettings.SKIP_FIELDS.contains(f.getName()) + || f.hasModifier(Modifier.PUBLIC) + || f.hasModifier(Modifier.TRANSIENT); } @Override diff --git a/jadx-gui/src/main/java/jadx/gui/settings/JadxSettingsWindow.java b/jadx-gui/src/main/java/jadx/gui/settings/JadxSettingsWindow.java index 37c405f91..89f281725 100644 --- a/jadx-gui/src/main/java/jadx/gui/settings/JadxSettingsWindow.java +++ b/jadx-gui/src/main/java/jadx/gui/settings/JadxSettingsWindow.java @@ -239,6 +239,14 @@ public class JadxSettingsWindow extends JDialog { } }); + JCheckBox fastSearch = new JCheckBox(); + fastSearch.setSelected(settings.isUseFastSearch()); + fastSearch.addItemListener(new ItemListener() { + public void itemStateChanged(ItemEvent e) { + settings.setUseFastSearch(e.getStateChange() == ItemEvent.SELECTED); + } + }); + SettingsGroup other = new SettingsGroup(NLS.str("preferences.other")); other.addRow(NLS.str("preferences.check_for_updates"), update); other.addRow(NLS.str("preferences.threads"), threadsCount); @@ -248,6 +256,7 @@ public class JadxSettingsWindow extends JDialog { other.addRow(NLS.str("preferences.cfg"), cfg); other.addRow(NLS.str("preferences.raw_cfg"), rawCfg); other.addRow(NLS.str("preferences.font"), fontBtn); + other.addRow(NLS.str("preferences.fast_search"), fastSearch); return other; } diff --git a/jadx-gui/src/main/java/jadx/gui/treemodel/CodeNode.java b/jadx-gui/src/main/java/jadx/gui/treemodel/CodeNode.java index 5b0378c55..192299791 100644 --- a/jadx-gui/src/main/java/jadx/gui/treemodel/CodeNode.java +++ b/jadx-gui/src/main/java/jadx/gui/treemodel/CodeNode.java @@ -1,27 +1,50 @@ package jadx.gui.treemodel; -import jadx.api.JavaClass; -import jadx.gui.utils.Utils; +import jadx.api.JavaNode; import javax.swing.Icon; -import javax.swing.ImageIcon; -public class CodeNode extends JClass { +public class CodeNode extends JNode { - private static final ImageIcon ICON = Utils.openIcon("file_obj"); + private static final long serialVersionUID = 1658650786734966545L; + private final JNode jNode; + private final JClass jParent; private final String line; private final int lineNum; - public CodeNode(JavaClass javaClass, int lineNum, String line) { - super(javaClass, (JClass) makeFrom(javaClass.getDeclaringClass())); + public CodeNode(JavaNode javaNode, int lineNum, String line) { + this.jNode = makeFrom(javaNode); + this.jParent = jNode.getJParent(); this.line = line; this.lineNum = lineNum; } @Override public Icon getIcon() { - return ICON; + return jNode.getIcon(); + } + + @Override + public JavaNode getJavaNode() { + return jNode.getJavaNode(); + } + + @Override + public JClass getJParent() { + return getRootClass(); + } + + @Override + public JClass getRootClass() { + JClass parent = jParent; + if (parent != null) { + return parent.getRootClass(); + } + if (jNode instanceof JClass) { + return (JClass) jNode; + } + return null; } @Override @@ -29,18 +52,23 @@ public class CodeNode extends JClass { return lineNum; } + @Override + public String makeDescString() { + return line; + } + + @Override + public boolean hasDescString() { + return true; + } + @Override public String makeString() { - return getCls().getFullName() + ":" + lineNum + " " + line; + return jNode.makeLongString(); } @Override public String makeLongString() { return makeString(); } - - @Override - public String toString() { - return makeString(); - } } diff --git a/jadx-gui/src/main/java/jadx/gui/treemodel/JClass.java b/jadx-gui/src/main/java/jadx/gui/treemodel/JClass.java index 68d1a4798..b7e532a42 100644 --- a/jadx-gui/src/main/java/jadx/gui/treemodel/JClass.java +++ b/jadx-gui/src/main/java/jadx/gui/treemodel/JClass.java @@ -3,6 +3,7 @@ package jadx.gui.treemodel; import jadx.api.JavaClass; import jadx.api.JavaField; import jadx.api.JavaMethod; +import jadx.api.JavaNode; import jadx.core.dex.info.AccessInfo; import jadx.gui.utils.NLS; import jadx.gui.utils.Utils; @@ -103,6 +104,11 @@ public class JClass extends JNode { return ICON_CLASS_DEFAULT; } + @Override + public JavaNode getJavaNode() { + return cls; + } + @Override public JClass getJParent() { return jParent; @@ -116,6 +122,11 @@ public class JClass extends JNode { return jParent.getRootClass(); } + @Override + public String getName() { + return cls.getName(); + } + public String getFullName() { return cls.getFullName(); } diff --git a/jadx-gui/src/main/java/jadx/gui/treemodel/JField.java b/jadx-gui/src/main/java/jadx/gui/treemodel/JField.java index c8b9ad929..589901d7d 100644 --- a/jadx-gui/src/main/java/jadx/gui/treemodel/JField.java +++ b/jadx-gui/src/main/java/jadx/gui/treemodel/JField.java @@ -1,6 +1,7 @@ package jadx.gui.treemodel; import jadx.api.JavaField; +import jadx.api.JavaNode; import jadx.core.dex.info.AccessInfo; import jadx.gui.utils.OverlayIcon; import jadx.gui.utils.Utils; @@ -27,6 +28,11 @@ public class JField extends JNode { this.jParent = jClass; } + @Override + public JavaNode getJavaNode() { + return field; + } + @Override public JClass getJParent() { return jParent; @@ -64,4 +70,14 @@ public class JField extends JNode { public String makeLongString() { return Utils.typeFormat(field.getFullName(), field.getType()); } + + @Override + public int hashCode() { + return field.hashCode(); + } + + @Override + public boolean equals(Object o) { + return this == o || o instanceof JField && field.equals(((JField) o).field); + } } diff --git a/jadx-gui/src/main/java/jadx/gui/treemodel/JMethod.java b/jadx-gui/src/main/java/jadx/gui/treemodel/JMethod.java index 8061bf9e9..ff65d890e 100644 --- a/jadx-gui/src/main/java/jadx/gui/treemodel/JMethod.java +++ b/jadx-gui/src/main/java/jadx/gui/treemodel/JMethod.java @@ -1,6 +1,7 @@ package jadx.gui.treemodel; import jadx.api.JavaMethod; +import jadx.api.JavaNode; import jadx.core.dex.info.AccessInfo; import jadx.core.dex.instructions.args.ArgType; import jadx.gui.utils.OverlayIcon; @@ -29,11 +30,20 @@ public class JMethod extends JNode { this.jParent = jClass; } + @Override + public JavaNode getJavaNode() { + return mth; + } + @Override public JClass getJParent() { return jParent; } + public ArgType getReturnType() { + return mth.getReturnType(); + } + @Override public JClass getRootClass() { return jParent.getRootClass(); @@ -57,7 +67,7 @@ public class JMethod extends JNode { return icon; } - private String makeBaseString() { + String makeBaseString() { if (mth.isClassInit()) { return "{...}"; } @@ -80,12 +90,22 @@ public class JMethod extends JNode { @Override public String makeString() { - return Utils.typeFormat(makeBaseString(), mth.getReturnType()); + return Utils.typeFormat(makeBaseString(), getReturnType()); } @Override public String makeLongString() { String name = mth.getDeclaringClass().getFullName() + "." + makeBaseString(); - return Utils.typeFormat(name, mth.getReturnType()); + return Utils.typeFormat(name, getReturnType()); + } + + @Override + public int hashCode() { + return mth.hashCode(); + } + + @Override + public boolean equals(Object o) { + return this == o || o instanceof JMethod && mth.equals(((JMethod) o).mth); } } diff --git a/jadx-gui/src/main/java/jadx/gui/treemodel/JNode.java b/jadx-gui/src/main/java/jadx/gui/treemodel/JNode.java index ca4459662..82e6ecb15 100644 --- a/jadx-gui/src/main/java/jadx/gui/treemodel/JNode.java +++ b/jadx-gui/src/main/java/jadx/gui/treemodel/JNode.java @@ -13,20 +13,20 @@ import org.fife.ui.rsyntaxtextarea.SyntaxConstants; public abstract class JNode extends DefaultMutableTreeNode { public static JNode makeFrom(JavaNode node) { + if (node == null) { + return null; + } if (node instanceof JavaClass) { JClass p = (JClass) makeFrom(node.getDeclaringClass()); return new JClass((JavaClass) node, p); } if (node instanceof JavaMethod) { JavaMethod mth = (JavaMethod) node; - return new JMethod(mth, new JClass(mth.getDeclaringClass())); + return new JMethod(mth, (JClass) makeFrom(mth.getDeclaringClass())); } if (node instanceof JavaField) { JavaField fld = (JavaField) node; - return new JField(fld, new JClass(fld.getDeclaringClass())); - } - if (node == null) { - return null; + return new JField(fld, (JClass) makeFrom(fld.getDeclaringClass())); } throw new JadxRuntimeException("Unknown type for JavaNode: " + node.getClass()); } @@ -40,6 +40,10 @@ public abstract class JNode extends DefaultMutableTreeNode { return null; } + public JavaNode getJavaNode() { + return null; + } + public String getContent() { return null; } @@ -58,8 +62,24 @@ public abstract class JNode extends DefaultMutableTreeNode { public abstract Icon getIcon(); + public String getName() { + JavaNode javaNode = getJavaNode(); + if (javaNode == null) { + return null; + } + return javaNode.getName(); + } + public abstract String makeString(); + public String makeDescString() { + return null; + } + + public boolean hasDescString() { + return false; + } + public String makeLongString() { return makeString(); } diff --git a/jadx-gui/src/main/java/jadx/gui/treemodel/JPackage.java b/jadx-gui/src/main/java/jadx/gui/treemodel/JPackage.java index a0cf53d23..066f101c6 100644 --- a/jadx-gui/src/main/java/jadx/gui/treemodel/JPackage.java +++ b/jadx-gui/src/main/java/jadx/gui/treemodel/JPackage.java @@ -9,6 +9,8 @@ import javax.swing.ImageIcon; import java.util.ArrayList; import java.util.List; +import org.jetbrains.annotations.NotNull; + public class JPackage extends JNode implements Comparable { private static final long serialVersionUID = -4120718634156839804L; @@ -45,6 +47,7 @@ public class JPackage extends JNode implements Comparable { } } + @Override public String getName() { return name; } @@ -77,7 +80,7 @@ public class JPackage extends JNode implements Comparable { } @Override - public int compareTo(JPackage o) { + public int compareTo(@NotNull JPackage o) { return name.compareTo(o.name); } diff --git a/jadx-gui/src/main/java/jadx/gui/treemodel/JResource.java b/jadx-gui/src/main/java/jadx/gui/treemodel/JResource.java index ca54bdfb9..395a4d495 100644 --- a/jadx-gui/src/main/java/jadx/gui/treemodel/JResource.java +++ b/jadx-gui/src/main/java/jadx/gui/treemodel/JResource.java @@ -24,7 +24,7 @@ public class JResource extends JNode implements Comparable { private static final ImageIcon JAVA_ICON = Utils.openIcon("java_ovr"); private static final ImageIcon ERROR_ICON = Utils.openIcon("error_co"); - public static enum JResType { + public enum JResType { ROOT, DIR, FILE diff --git a/jadx-gui/src/main/java/jadx/gui/ui/CommonSearchDialog.java b/jadx-gui/src/main/java/jadx/gui/ui/CommonSearchDialog.java new file mode 100644 index 000000000..d4c6b4852 --- /dev/null +++ b/jadx-gui/src/main/java/jadx/gui/ui/CommonSearchDialog.java @@ -0,0 +1,394 @@ +package jadx.gui.ui; + +import jadx.gui.jobs.BackgroundJob; +import jadx.gui.jobs.DecompileJob; +import jadx.gui.treemodel.JNode; +import jadx.gui.treemodel.TextNode; +import jadx.gui.utils.CacheObject; +import jadx.gui.utils.NLS; +import jadx.gui.utils.Position; +import jadx.gui.utils.TextSearchIndex; + +import javax.swing.BorderFactory; +import javax.swing.Box; +import javax.swing.BoxLayout; +import javax.swing.JButton; +import javax.swing.JComponent; +import javax.swing.JDialog; +import javax.swing.JLabel; +import javax.swing.JPanel; +import javax.swing.JScrollPane; +import javax.swing.JTable; +import javax.swing.KeyStroke; +import javax.swing.ListSelectionModel; +import javax.swing.ScrollPaneConstants; +import javax.swing.SwingWorker; +import javax.swing.UIDefaults; +import javax.swing.UIManager; +import javax.swing.table.AbstractTableModel; +import javax.swing.table.TableCellRenderer; +import javax.swing.table.TableColumn; +import javax.swing.table.TableColumnModel; +import java.awt.Color; +import java.awt.Component; +import java.awt.Cursor; +import java.awt.Dimension; +import java.awt.Font; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.awt.event.KeyAdapter; +import java.awt.event.KeyEvent; +import java.awt.event.MouseAdapter; +import java.awt.event.MouseEvent; +import java.util.ArrayList; +import java.util.List; + +import org.fife.ui.rsyntaxtextarea.RSyntaxTextArea; +import org.fife.ui.rsyntaxtextarea.SyntaxConstants; +import org.fife.ui.rtextarea.SearchContext; +import org.fife.ui.rtextarea.SearchEngine; +import org.jetbrains.annotations.NotNull; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public abstract class CommonSearchDialog extends JDialog { + + private static final Logger LOG = LoggerFactory.getLogger(CommonSearchDialog.class); + private static final long serialVersionUID = 8939332306115370276L; + + public static final int MAX_RESULTS_COUNT = 1000; + + protected final TabbedPane tabbedPane; + protected final CacheObject cache; + protected final MainWindow mainWindow; + protected final Font codeFont; + + protected ResultsModel resultsModel; + protected ResultsTable resultsTable; + protected JLabel warnLabel; + protected ProgressPanel progressPane; + + protected String highlightText; + + public CommonSearchDialog(MainWindow mainWindow) { + super(mainWindow); + this.mainWindow = mainWindow; + this.tabbedPane = mainWindow.getTabbedPane(); + this.cache = mainWindow.getCacheObject(); + this.codeFont = mainWindow.getSettings().getFont(); + } + + public void prepare() { + if (cache.getIndexJob().isComplete()) { + loadFinishedCommon(); + loadFinished(); + return; + } + LoadTask task = new LoadTask(); + task.addPropertyChangeListener(progressPane); + task.execute(); + } + + protected void openSelectedItem() { + int selectedId = resultsTable.getSelectedRow(); + if (selectedId == -1) { + return; + } + JNode node = (JNode) resultsModel.getValueAt(selectedId, 0); + tabbedPane.codeJump(new Position(node.getRootClass(), node.getLine())); + + dispose(); + } + + protected void initCommon() { + KeyStroke stroke = KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0); + getRootPane().registerKeyboardAction(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + dispose(); + } + }, stroke, JComponent.WHEN_IN_FOCUSED_WINDOW); + } + + @NotNull + protected JPanel initButtonsPanel() { + progressPane = new ProgressPanel(mainWindow, false); + + JButton cancelButton = new JButton(NLS.str("search_dialog.cancel")); + cancelButton.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent event) { + dispose(); + } + }); + JButton openBtn = new JButton(NLS.str("search_dialog.open")); + openBtn.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent 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)); + buttonPane.add(progressPane); + buttonPane.add(Box.createRigidArea(new Dimension(5, 0))); + buttonPane.add(Box.createHorizontalGlue()); + buttonPane.add(openBtn); + buttonPane.add(Box.createRigidArea(new Dimension(10, 0))); + buttonPane.add(cancelButton); + return buttonPane; + } + + protected JPanel initResultsTable() { + resultsModel = new ResultsModel(); + resultsTable = new ResultsTable(resultsModel); + resultsTable.setShowHorizontalLines(false); +// resultsTable.setAutoCreateColumnsFromModel(true); + resultsTable.setDragEnabled(false); + resultsTable.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); + resultsTable.setBackground(ContentArea.BACKGROUND); + resultsTable.setColumnSelectionAllowed(false); + resultsTable.setAutoResizeMode(JTable.AUTO_RESIZE_OFF); + resultsTable.setAutoscrolls(false); + resultsTable.setDefaultRenderer(Object.class, new ResultsTableCellRenderer()); + resultsTable.addMouseListener(new MouseAdapter() { + public void mouseClicked(MouseEvent evt) { + if (evt.getClickCount() == 2) { + openSelectedItem(); + } + } + }); + resultsTable.addKeyListener(new KeyAdapter() { + @Override + public void keyPressed(KeyEvent e) { + if (e.getKeyCode() == KeyEvent.VK_ENTER) { + openSelectedItem(); + } + } + }); + + warnLabel = new JLabel(); + warnLabel.setForeground(Color.RED); + warnLabel.setVisible(false); + + JPanel resultsPanel = new JPanel(); + resultsPanel.setLayout(new BoxLayout(resultsPanel, BoxLayout.PAGE_AXIS)); + resultsPanel.add(warnLabel); + resultsPanel.add(new JScrollPane(resultsTable, + ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED, + ScrollPaneConstants.HORIZONTAL_SCROLLBAR_AS_NEEDED)); + resultsPanel.setBorder(BorderFactory.createEmptyBorder(0, 10, 10, 10)); + return resultsPanel; + } + + protected static class ResultsTable extends JTable { + private static final long serialVersionUID = 3901184054736618969L; + + public ResultsTable(ResultsModel resultsModel) { + super(resultsModel); + } + + public void updateTable() { + ResultsModel model = (ResultsModel) getModel(); + TableColumnModel columnModel = getColumnModel(); + + int width = getParent().getWidth(); + int firstColMaxWidth = (int) (width * 0.5); + int rowCount = getRowCount(); + int columnCount = getColumnCount(); + if (!model.isAddDescColumn()) { + firstColMaxWidth = width; + } + Component codeComp = null; + for (int col = 0; col < columnCount; col++) { + int colWidth = 50; + for (int row = 0; row < rowCount; row++) { + TableCellRenderer renderer = getCellRenderer(row, col); + Component comp = prepareRenderer(renderer, row, col); + if (comp == null) { + continue; + } + colWidth = Math.max(comp.getPreferredSize().width, colWidth); + if (codeComp == null && col == 1) { + codeComp = comp; + } + } + colWidth += 10; + if (col == 0) { + colWidth = Math.min(colWidth, firstColMaxWidth); + } else { + colWidth = Math.max(colWidth, width - columnModel.getColumn(0).getPreferredWidth()); + } + TableColumn column = columnModel.getColumn(col); + column.setPreferredWidth(colWidth); + } + if (codeComp != null) { + setRowHeight(codeComp.getPreferredSize().height + 4); + } + updateUI(); + } + } + + protected static class ResultsModel extends AbstractTableModel { + private static final long serialVersionUID = -7821286846923903208L; + private static final String[] COLUMN_NAMES = {"Node", "Code"}; + + private final List rows = new ArrayList(); + private boolean addDescColumn; + + protected void addAll(Iterable nodes) { + for (JNode node : nodes) { + int size = getRowCount(); + if (size >= MAX_RESULTS_COUNT) { + if (size == MAX_RESULTS_COUNT) { + add(new TextNode("Search results truncated (limit: " + MAX_RESULTS_COUNT + ")")); + } + return; + } + add(node); + } + } + + private void add(JNode node) { + if (node.hasDescString()) { + addDescColumn = true; + } + rows.add(node); + } + + public void clear() { + addDescColumn = false; + rows.clear(); + } + + public boolean isAddDescColumn() { + return addDescColumn; + } + + @Override + public int getRowCount() { + return rows.size(); + } + + @Override + public int getColumnCount() { + return 2; + } + + @Override + public String getColumnName(int index) { + return COLUMN_NAMES[index]; + } + + @Override + public Object getValueAt(int rowIndex, int columnIndex) { + return rows.get(rowIndex); + } + } + + protected class ResultsTableCellRenderer implements TableCellRenderer { + private final Color selectedBackground; + private final Color selectedForeground; + + ResultsTableCellRenderer() { + UIDefaults defaults = UIManager.getDefaults(); + selectedBackground = defaults.getColor("List.selectionBackground"); + selectedForeground = defaults.getColor("List.selectionForeground"); + } + + @Override + public Component getTableCellRendererComponent(JTable table, Object obj, boolean isSelected, + boolean hasFocus, int row, int column) { + if (!(obj instanceof JNode)) { + return null; + } + JNode node = (JNode) obj; + + if (column == 0) { + JLabel label = new JLabel(); + label.setOpaque(true); + if (isSelected) { + label.setBackground(selectedBackground); + label.setForeground(selectedForeground); + } else { + label.setBackground(ContentArea.BACKGROUND); + } + label.setIcon(node.getIcon()); + label.setText(node.makeLongString() + " "); + return label; + } + if (node.hasDescString()) { + RSyntaxTextArea textArea = new RSyntaxTextArea(); + textArea.setFont(codeFont); + textArea.setEditable(false); + textArea.setSyntaxEditingStyle(SyntaxConstants.SYNTAX_STYLE_JAVA); + textArea.setBackground(isSelected ? selectedBackground : ContentArea.BACKGROUND); + textArea.setText(" " + node.makeDescString()); + textArea.setRows(1); + textArea.setColumns(textArea.getText().length()); + if (highlightText != null) { + SearchEngine.markAll(textArea, new SearchContext(highlightText)); + } + return textArea; + } + return null; + } + } + + private class LoadTask extends SwingWorker { + public LoadTask() { + loadStartCommon(); + loadStart(); + } + + @Override + public Void doInBackground() { + try { + mainWindow.getBackgroundWorker().exec(); + + DecompileJob decompileJob = cache.getDecompileJob(); + progressPane.changeLabel(this, decompileJob.getInfoString()); + decompileJob.processAndWait(); + + BackgroundJob indexJob = cache.getIndexJob(); + progressPane.changeLabel(this, indexJob.getInfoString()); + indexJob.processAndWait(); + } catch (Exception e) { + LOG.error("Waiting background tasks failed", e); + } + return null; + } + + @Override + public void done() { + loadFinishedCommon(); + loadFinished(); + } + } + + protected void loadStartCommon() { + setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR)); + progressPane.setIndeterminate(true); + progressPane.setVisible(true); + resultsTable.setEnabled(false); + warnLabel.setVisible(false); + } + + private void loadFinishedCommon() { + setCursor(null); + resultsTable.setEnabled(true); + progressPane.setVisible(false); + + TextSearchIndex textIndex = cache.getTextIndex(); + if (textIndex == null) { + warnLabel.setText("Index not initialized, search will be disabled!"); + warnLabel.setVisible(true); + } + } + + protected abstract void loadFinished(); + + protected abstract void loadStart(); + +} diff --git a/jadx-gui/src/main/java/jadx/gui/ui/ContentArea.java b/jadx-gui/src/main/java/jadx/gui/ui/ContentArea.java index 09b55d99a..b79654ddb 100644 --- a/jadx-gui/src/main/java/jadx/gui/ui/ContentArea.java +++ b/jadx-gui/src/main/java/jadx/gui/ui/ContentArea.java @@ -1,15 +1,21 @@ package jadx.gui.ui; import jadx.api.CodePosition; +import jadx.api.JavaNode; import jadx.gui.settings.JadxSettings; import jadx.gui.treemodel.JClass; import jadx.gui.treemodel.JNode; import jadx.gui.utils.Position; +import javax.swing.AbstractAction; +import javax.swing.Action; +import javax.swing.JPopupMenu; import javax.swing.JViewport; import javax.swing.SwingUtilities; import javax.swing.event.HyperlinkEvent; import javax.swing.event.HyperlinkListener; +import javax.swing.event.PopupMenuEvent; +import javax.swing.event.PopupMenuListener; import javax.swing.text.BadLocationException; import javax.swing.text.Caret; import javax.swing.text.DefaultCaret; @@ -17,6 +23,7 @@ import java.awt.Color; import java.awt.Dimension; import java.awt.Point; import java.awt.Rectangle; +import java.awt.event.ActionEvent; import org.fife.ui.rsyntaxtextarea.LinkGenerator; import org.fife.ui.rsyntaxtextarea.LinkGeneratorResult; @@ -63,11 +70,23 @@ class ContentArea extends RSyntaxTextArea { CodeLinkGenerator codeLinkProcessor = new CodeLinkGenerator((JClass) node); setLinkGenerator(codeLinkProcessor); addHyperlinkListener(codeLinkProcessor); + addMenuItems(this, (JClass) node); } setText(node.getContent()); } + private void addMenuItems(ContentArea contentArea, JClass jCls) { + Action findUsage = new FindUsageAction(contentArea, jCls); + // TODO: hotkey works only when popup menu is shown + // findUsage.putValue(Action.ACCELERATOR_KEY, getKeyStroke(KeyEvent.VK_F7, KeyEvent.ALT_DOWN_MASK)); + + JPopupMenu popup = getPopupMenu(); + popup.addSeparator(); + popup.add(findUsage); + popup.addPopupMenuListener((PopupMenuListener) findUsage); + } + public void loadSettings() { JadxSettings settings = contentPanel.getTabbedPane().getMainWindow().getSettings(); setFont(settings.getFont()); @@ -83,7 +102,7 @@ class ContentArea extends RSyntaxTextArea { } } if (node instanceof JClass) { - Position pos = getPosition((JClass) node, this, token.getOffset()); + Position pos = getDefPosition((JClass) node, this, token.getOffset()); if (pos != null) { return true; } @@ -100,21 +119,30 @@ class ContentArea extends RSyntaxTextArea { return super.getForegroundForToken(t); } - static Position getPosition(JClass jCls, RSyntaxTextArea textArea, int offset) { + static Position getDefPosition(JClass jCls, RSyntaxTextArea textArea, int offset) { + JavaNode node = getJavaNodeAtOffset(jCls, textArea, offset); + if (node == null) { + return null; + } + CodePosition pos = jCls.getCls().getDefinitionPosition(node); + if (pos == null) { + return null; + } + return new Position(pos); + } + + static JavaNode getJavaNodeAtOffset(JClass jCls, RSyntaxTextArea textArea, int offset) { try { int line = textArea.getLineOfOffset(offset); int lineOffset = offset - textArea.getLineStartOffset(line); - CodePosition pos = jCls.getCls().getDefinitionPosition(line + 1, lineOffset + 1); - if (pos != null && pos.isSet()) { - return new Position(pos); - } + return jCls.getCls().getJavaNodeAtPosition(line + 1, lineOffset + 1); } catch (BadLocationException e) { - LOG.error("Can't get line by offset", e); + LOG.error("Can't get java node by offset", e); } return null; } - Position getCurrentPosition() { + public Position getCurrentPosition() { return new Position(node, getCaretLineNumber() + 1); } @@ -166,6 +194,52 @@ class ContentArea extends RSyntaxTextArea { } } + private class FindUsageAction extends AbstractAction implements PopupMenuListener { + private static final long serialVersionUID = 4692546569977976384L; + + private final ContentArea contentArea; + private final JClass jCls; + + private JavaNode node; + + public FindUsageAction(ContentArea contentArea, JClass jCls) { + super("Find Usage"); + this.contentArea = contentArea; + this.jCls = jCls; + } + + @Override + public void actionPerformed(ActionEvent e) { + if (node == null) { + return; + } + MainWindow mainWindow = contentPanel.getTabbedPane().getMainWindow(); + UsageDialog usageDialog = new UsageDialog(mainWindow, JNode.makeFrom(node)); + usageDialog.setVisible(true); + } + + @Override + public void popupMenuWillBecomeVisible(PopupMenuEvent e) { + node = null; + Point pos = contentArea.getMousePosition(); + if (pos != null) { + Token token = contentArea.viewToToken(pos); + if (token != null) { + node = getJavaNodeAtOffset(jCls, contentArea, token.getOffset()); + } + } + setEnabled(node != null); + } + + @Override + public void popupMenuWillBecomeInvisible(PopupMenuEvent e) { + } + + @Override + public void popupMenuCanceled(PopupMenuEvent e) { + } + } + private class CodeLinkGenerator implements LinkGenerator, HyperlinkListener { private final JClass jCls; @@ -181,7 +255,7 @@ class ContentArea extends RSyntaxTextArea { return null; } final int sourceOffset = token.getOffset(); - final Position defPos = getPosition(jCls, textArea, sourceOffset); + final Position defPos = getDefPosition(jCls, textArea, sourceOffset); if (defPos == null) { return null; } @@ -207,12 +281,7 @@ class ContentArea extends RSyntaxTextArea { public void hyperlinkUpdate(HyperlinkEvent e) { Object obj = e.getSource(); if (obj instanceof Position) { - Position pos = (Position) obj; - LOG.debug("Code jump to: {}", pos); - TabbedPane tabbedPane = contentPanel.getTabbedPane(); - tabbedPane.getJumpManager().addPosition(getCurrentPosition()); - tabbedPane.getJumpManager().addPosition(pos); - tabbedPane.showCode(pos); + contentPanel.getTabbedPane().codeJump((Position) obj); } } } diff --git a/jadx-gui/src/main/java/jadx/gui/ui/MainDropTarget.java b/jadx-gui/src/main/java/jadx/gui/ui/MainDropTarget.java index bc7ac8bb2..604b2615f 100644 --- a/jadx-gui/src/main/java/jadx/gui/ui/MainDropTarget.java +++ b/jadx-gui/src/main/java/jadx/gui/ui/MainDropTarget.java @@ -23,7 +23,6 @@ public class MainDropTarget implements DropTargetListener { private final MainWindow mainWindow; public MainDropTarget(MainWindow mainWindow) { - super(); this.mainWindow = mainWindow; } @@ -50,6 +49,7 @@ public class MainDropTarget implements DropTargetListener { } @Override + @SuppressWarnings("unchecked") public void drop(DropTargetDropEvent dtde) { if (!dtde.isDataFlavorSupported(DataFlavor.javaFileListFlavor)) { dtde.rejectDrop(); @@ -57,7 +57,6 @@ public class MainDropTarget implements DropTargetListener { } dtde.acceptDrop(dtde.getDropAction()); try { - Transferable transferable = dtde.getTransferable(); List transferData = (List) transferable.getTransferData(DataFlavor.javaFileListFlavor); if (transferData != null && transferData.size() > 0) { @@ -65,7 +64,6 @@ public class MainDropTarget implements DropTargetListener { // load first file mainWindow.openFile(transferData.get(0)); } - } catch (Exception e) { LOG.error("File drop operation failed", e); } @@ -73,7 +71,5 @@ public class MainDropTarget implements DropTargetListener { @Override public void dragExit(DropTargetEvent dte) { - } - } diff --git a/jadx-gui/src/main/java/jadx/gui/ui/MainWindow.java b/jadx-gui/src/main/java/jadx/gui/ui/MainWindow.java index b7507218b..00937d071 100644 --- a/jadx-gui/src/main/java/jadx/gui/ui/MainWindow.java +++ b/jadx-gui/src/main/java/jadx/gui/ui/MainWindow.java @@ -1,6 +1,9 @@ package jadx.gui.ui; import jadx.gui.JadxWrapper; +import jadx.gui.jobs.BackgroundWorker; +import jadx.gui.jobs.DecompileJob; +import jadx.gui.jobs.IndexJob; import jadx.gui.settings.JadxSettings; import jadx.gui.settings.JadxSettingsWindow; import jadx.gui.treemodel.JClass; @@ -48,7 +51,6 @@ import javax.swing.tree.ExpandVetoException; import javax.swing.tree.TreeNode; import javax.swing.tree.TreePath; import javax.swing.tree.TreeSelectionModel; - import java.awt.BorderLayout; import java.awt.Component; import java.awt.DisplayMode; @@ -66,6 +68,8 @@ import java.awt.event.MouseEvent; import java.io.File; import java.util.Arrays; import java.util.EnumSet; +import java.util.Timer; +import java.util.TimerTask; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -111,7 +115,9 @@ public class MainWindow extends JFrame { private JToggleButton deobfToggleBtn; private boolean isFlattenPackage; private Link updateLink; - + private ProgressPanel progressPane; + private BackgroundWorker backgroundWorker; + private DropTarget dropTarget; public MainWindow(JadxSettings settings) { @@ -119,6 +125,7 @@ public class MainWindow extends JFrame { this.settings = settings; this.cacheObject = new CacheObject(); + resetCache(); initUI(); initMenuAndToolbar(); checkForUpdate(); @@ -175,18 +182,45 @@ public class MainWindow extends JFrame { } public void openFile(File file) { - cacheObject.reset(); + tabbedPane.closeAllTabs(); + resetCache(); wrapper.openFile(file); deobfToggleBtn.setSelected(settings.isDeobfuscationOn()); settings.addRecentFile(file.getAbsolutePath()); initTree(); setTitle(DEFAULT_TITLE + " - " + file.getName()); + runBackgroundJobs(); + } + + protected void resetCache() { + cacheObject.reset(); + int threadsCount = settings.getThreadsCount(); + cacheObject.setDecompileJob(new DecompileJob(wrapper, threadsCount)); + cacheObject.setIndexJob(new IndexJob(wrapper, settings, cacheObject)); + } + + private synchronized void runBackgroundJobs() { + cancelBackgroundJobs(); + backgroundWorker = new BackgroundWorker(cacheObject, progressPane); + new Timer().schedule(new TimerTask() { + @Override + public void run() { + backgroundWorker.exec(); + } + }, 1000); + } + + public synchronized void cancelBackgroundJobs() { + if (backgroundWorker != null) { + backgroundWorker.stop(); + backgroundWorker = new BackgroundWorker(cacheObject, progressPane); + resetCache(); + } } public void reOpenFile() { File openedFile = wrapper.getOpenFile(); if (openedFile != null) { - tabbedPane.closeAllTabs(); openFile(openedFile); } } @@ -247,14 +281,14 @@ public class MainWindow extends JFrame { if (obj instanceof JResource) { JResource res = (JResource) obj; if (res.getContent() != null) { - tabbedPane.showCode(new Position(res, res.getLine())); + tabbedPane.codeJump(new Position(res, res.getLine())); } } if (obj instanceof JNode) { JNode node = (JNode) obj; JClass cls = node.getRootClass(); if (cls != null) { - tabbedPane.showCode(new Position(cls, node.getLine())); + tabbedPane.codeJump(new Position(cls, node.getLine())); } } } catch (Exception e) { @@ -539,14 +573,18 @@ public class MainWindow extends JFrame { } }); - JScrollPane treeScrollPane = new JScrollPane(tree); - splitPane.setLeftComponent(treeScrollPane); + progressPane = new ProgressPanel(this, true); + + JPanel leftPane = new JPanel(new BorderLayout()); + leftPane.add(new JScrollPane(tree), BorderLayout.CENTER); + leftPane.add(progressPane, BorderLayout.PAGE_END); + splitPane.setLeftComponent(leftPane); tabbedPane = new TabbedPane(this); splitPane.setRightComponent(tabbedPane); dropTarget = new DropTarget(this, DnDConstants.ACTION_COPY, new MainDropTarget(this)); - + setContentPane(mainPanel); setTitle(DEFAULT_TITLE); } @@ -565,6 +603,12 @@ public class MainWindow extends JFrame { tabbedPane.loadSettings(); } + @Override + public void dispose() { + cancelBackgroundJobs(); + super.dispose(); + } + public JadxWrapper getWrapper() { return wrapper; } @@ -581,6 +625,10 @@ public class MainWindow extends JFrame { return cacheObject; } + public BackgroundWorker getBackgroundWorker() { + return backgroundWorker; + } + private class RecentFilesMenuListener implements MenuListener { private final JMenu recentFiles; @@ -619,4 +667,5 @@ public class MainWindow extends JFrame { public void menuCanceled(MenuEvent e) { } } + } diff --git a/jadx-gui/src/main/java/jadx/gui/ui/ProgressPanel.java b/jadx-gui/src/main/java/jadx/gui/ui/ProgressPanel.java new file mode 100644 index 000000000..6a01c2bc4 --- /dev/null +++ b/jadx-gui/src/main/java/jadx/gui/ui/ProgressPanel.java @@ -0,0 +1,82 @@ +package jadx.gui.ui; + +import jadx.gui.utils.Utils; + +import javax.swing.BorderFactory; +import javax.swing.BoxLayout; +import javax.swing.Icon; +import javax.swing.JButton; +import javax.swing.JLabel; +import javax.swing.JPanel; +import javax.swing.JProgressBar; +import javax.swing.SwingWorker; +import java.awt.Dimension; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.beans.PropertyChangeEvent; +import java.beans.PropertyChangeListener; + +public class ProgressPanel extends JPanel implements PropertyChangeListener { + + private static final long serialVersionUID = -3238438119672015733L; + + private static final Icon ICON_CANCEL = Utils.openIcon("cross"); + + private final JProgressBar progressBar; + private final JLabel progressLabel; + + public ProgressPanel(final MainWindow mainWindow, boolean showCancelButton) { + progressLabel = new JLabel(); + progressBar = new JProgressBar(0, 100); + progressBar.setIndeterminate(true); + progressBar.setStringPainted(false); + progressLabel.setLabelFor(progressBar); + + setBorder(BorderFactory.createEmptyBorder(2, 2, 2, 2)); + setLayout(new BoxLayout(this, BoxLayout.X_AXIS)); + setVisible(false); + add(progressLabel); + add(progressBar); + + if (showCancelButton) { + JButton cancelButton = new JButton(ICON_CANCEL); + cancelButton.setPreferredSize(new Dimension(ICON_CANCEL.getIconWidth(), ICON_CANCEL.getIconHeight())); + cancelButton.setToolTipText("Cancel background jobs"); + cancelButton.setBorderPainted(false); + cancelButton.setFocusPainted(false); + cancelButton.setContentAreaFilled(false); + cancelButton.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + mainWindow.cancelBackgroundJobs(); + } + }); + add(cancelButton); + } + } + + @Override + public void propertyChange(PropertyChangeEvent evt) { + if ("progress".equals(evt.getPropertyName())) { + int progress = (Integer) evt.getNewValue(); + progressBar.setIndeterminate(false); + progressBar.setValue(progress); + progressBar.setString(progress + "%"); + progressBar.setStringPainted(true); + } else if ("label".equals(evt.getPropertyName())) { + setLabel((String) evt.getNewValue()); + } + } + + public void setLabel(String label) { + progressLabel.setText(label); + } + + public void setIndeterminate(boolean newValue) { + progressBar.setIndeterminate(newValue); + } + + public void changeLabel(SwingWorker task, String label) { + task.firePropertyChange("label", null, label); + } +} diff --git a/jadx-gui/src/main/java/jadx/gui/ui/SearchDialog.java b/jadx-gui/src/main/java/jadx/gui/ui/SearchDialog.java index 4f0a959ed..564cba57f 100644 --- a/jadx-gui/src/main/java/jadx/gui/ui/SearchDialog.java +++ b/jadx-gui/src/main/java/jadx/gui/ui/SearchDialog.java @@ -1,53 +1,28 @@ package jadx.gui.ui; -import jadx.api.JavaClass; -import jadx.gui.JadxWrapper; -import jadx.gui.treemodel.JNode; -import jadx.gui.treemodel.TextNode; -import jadx.gui.utils.CacheObject; import jadx.gui.utils.NLS; -import jadx.gui.utils.Position; import jadx.gui.utils.TextSearchIndex; import jadx.gui.utils.TextStandardActions; import javax.swing.BorderFactory; import javax.swing.Box; import javax.swing.BoxLayout; -import javax.swing.DefaultListModel; -import javax.swing.JButton; import javax.swing.JCheckBox; -import javax.swing.JComponent; -import javax.swing.JDialog; import javax.swing.JLabel; -import javax.swing.JList; import javax.swing.JPanel; -import javax.swing.JProgressBar; -import javax.swing.JScrollPane; import javax.swing.JTextField; -import javax.swing.KeyStroke; -import javax.swing.ListCellRenderer; import javax.swing.SwingUtilities; -import javax.swing.SwingWorker; -import javax.swing.UIDefaults; -import javax.swing.UIManager; import javax.swing.WindowConstants; import javax.swing.event.DocumentEvent; import javax.swing.event.DocumentListener; import java.awt.BorderLayout; -import java.awt.Color; -import java.awt.Component; import java.awt.Container; -import java.awt.Cursor; import java.awt.Dimension; import java.awt.FlowLayout; -import java.awt.event.ActionEvent; -import java.awt.event.ActionListener; import java.awt.event.ItemEvent; import java.awt.event.ItemListener; import java.awt.event.KeyAdapter; import java.awt.event.KeyEvent; -import java.awt.event.MouseAdapter; -import java.awt.event.MouseEvent; import java.awt.event.WindowAdapter; import java.awt.event.WindowEvent; import java.util.EnumSet; @@ -56,12 +31,11 @@ import java.util.Set; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -public class SearchDialog extends JDialog { +public class SearchDialog extends CommonSearchDialog { private static final long serialVersionUID = -5105405456969134105L; private static final Logger LOG = LoggerFactory.getLogger(SearchDialog.class); - private static final int MAX_RESULTS_COUNT = 500; enum SearchOptions { CLASS, @@ -72,67 +46,43 @@ public class SearchDialog extends JDialog { private Set options = EnumSet.allOf(SearchOptions.class); - private final TabbedPane tabbedPane; - private final JadxWrapper wrapper; - private final CacheObject cache; - private JTextField searchField; - private ResultsModel resultsModel; - private JList resultsList; - private JProgressBar busyBar; public SearchDialog(MainWindow mainWindow, Set options) { super(mainWindow); - this.tabbedPane = mainWindow.getTabbedPane(); - this.wrapper = mainWindow.getWrapper(); - this.cache = mainWindow.getCacheObject(); this.options = options; initUI(); addWindowListener(new WindowAdapter() { @Override - public void windowActivated(WindowEvent e) { + public void windowOpened(WindowEvent e) { SwingUtilities.invokeLater(new Runnable() { @Override public void run() { - prepare(); - searchField.requestFocus(); + openInit(); } }); } }); } - public void prepare() { - TextSearchIndex index = cache.getTextIndex(); - if (index != null) { - return; + protected void openInit() { + prepare(); + String lastSearch = cache.getLastSearch(); + if (lastSearch != null) { + searchField.setText(lastSearch); + searchField.selectAll(); } - LoadTask task = new LoadTask(); - task.execute(); - } - - private void loadData() { - TextSearchIndex index = cache.getTextIndex(); - if (index != null) { - return; - } - index = new TextSearchIndex(); - for (JavaClass cls : wrapper.getClasses()) { - index.indexNames(cls); - } - for (JavaClass cls : wrapper.getClasses()) { - index.indexCode(cls); - } - cache.setTextIndex(index); + searchField.requestFocus(); } private synchronized void performSearch() { - resultsModel.removeAllElements(); + resultsModel.clear(); String text = searchField.getText(); if (text == null || text.isEmpty() || options.isEmpty()) { return; } + cache.setLastSearch(text); TextSearchIndex index = cache.getTextIndex(); if (index == null) { return; @@ -149,92 +99,9 @@ public class SearchDialog extends JDialog { if (options.contains(SearchOptions.CODE)) { resultsModel.addAll(index.searchCode(text)); } - LOG.info("Search returned {} results", resultsModel.size()); + highlightText = text; + resultsTable.updateTable(); } - - private void openSelectedItem() { - int selectedId = resultsList.getSelectedIndex(); - if (selectedId == -1) { - return; - } - JNode node = (JNode) resultsModel.get(selectedId); - tabbedPane.showCode(new Position(node.getRootClass(), node.getLine())); - - dispose(); - } - - private class LoadTask extends SwingWorker { - public LoadTask() { - setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR)); - busyBar.setVisible(true); - searchField.setEnabled(false); - resultsList.setEnabled(false); - } - - @Override - public Void doInBackground() { - loadData(); - return null; - } - - @Override - public void done() { - SwingUtilities.invokeLater(new Runnable() { - public void run() { - setCursor(null); - searchField.setEnabled(true); - resultsList.setEnabled(true); - busyBar.setVisible(false); - } - }); - } - } - - private static class ResultsModel extends DefaultListModel { - private static final long serialVersionUID = -7821286846923903208L; - - private void addAll(Iterable nodes) { - for (JNode node : nodes) { - if (size() >= MAX_RESULTS_COUNT) { - if (size() == MAX_RESULTS_COUNT) { - addElement(new TextNode("Search results truncated (limit: " + MAX_RESULTS_COUNT + ")")); - } - return; - } - addElement(node); - } - } - } - - private static class ResultsCellRenderer implements ListCellRenderer { - private final Color selectedBackground; - private final Color selectedForeground; - - ResultsCellRenderer() { - UIDefaults defaults = UIManager.getDefaults(); - selectedBackground = defaults.getColor("List.selectionBackground"); - selectedForeground = defaults.getColor("List.selectionForeground"); - } - - @Override - public Component getListCellRendererComponent(JList list, - Object obj, int index, boolean isSelected, boolean cellHasFocus) { - if (!(obj instanceof JNode)) { - return null; - } - JNode value = (JNode) obj; - JLabel label = new JLabel(); - label.setOpaque(true); - label.setIcon(value.getIcon()); - label.setText(value.makeLongString()); - if (isSelected) { - label.setBackground(selectedBackground); - label.setForeground(selectedForeground); - } - return label; - } - } - private class SearchFieldListener implements DocumentListener { public void changedUpdate(DocumentEvent e) { @@ -263,17 +130,6 @@ public class SearchDialog extends JDialog { JCheckBox fldChBox = makeOptionsCheckBox(NLS.str("search_dialog.field"), SearchOptions.FIELD); JCheckBox codeChBox = makeOptionsCheckBox(NLS.str("search_dialog.code"), SearchOptions.CODE); - resultsModel = new ResultsModel(); - resultsList = new JList(resultsModel); - resultsList.setCellRenderer(new ResultsCellRenderer()); - resultsList.addMouseListener(new MouseAdapter() { - public void mouseClicked(MouseEvent evt) { - if (evt.getClickCount() == 2) { - openSelectedItem(); - } - } - }); - JPanel searchOptions = new JPanel(new FlowLayout(FlowLayout.LEFT)); searchOptions.setBorder(BorderFactory.createTitledBorder(NLS.str("search_dialog.search_in"))); searchOptions.add(clsChBox); @@ -292,68 +148,30 @@ public class SearchDialog extends JDialog { searchPane.add(searchOptions); searchPane.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10)); - JPanel listPane = new JPanel(); - listPane.setLayout(new BoxLayout(listPane, BoxLayout.PAGE_AXIS)); - listPane.add(new JScrollPane(resultsList)); - listPane.setBorder(BorderFactory.createEmptyBorder(0, 10, 10, 10)); - - busyBar = new JProgressBar(); - busyBar.setIndeterminate(true); - busyBar.setVisible(false); - - //Create and initialize the buttons. - JButton cancelButton = new JButton(NLS.str("search_dialog.cancel")); - cancelButton.addActionListener(new ActionListener() { - public void actionPerformed(ActionEvent event) { - dispose(); - } - }); - JButton openBtn = new JButton(NLS.str("search_dialog.open")); - openBtn.addActionListener(new ActionListener() { - public void actionPerformed(ActionEvent event) { - openSelectedItem(); - } - }); - - JPanel buttonPane = new JPanel(); - buttonPane.setLayout(new BoxLayout(buttonPane, BoxLayout.LINE_AXIS)); - buttonPane.setBorder(BorderFactory.createEmptyBorder(0, 10, 10, 10)); - buttonPane.add(busyBar); - buttonPane.add(Box.createRigidArea(new Dimension(5, 0))); - buttonPane.add(Box.createHorizontalGlue()); - buttonPane.add(openBtn); - buttonPane.add(Box.createRigidArea(new Dimension(10, 0))); - buttonPane.add(cancelButton); + initCommon(); + JPanel resultsPanel = initResultsTable(); + JPanel buttonPane = initButtonsPanel(); Container contentPane = getContentPane(); contentPane.add(searchPane, BorderLayout.PAGE_START); - contentPane.add(listPane, BorderLayout.CENTER); + contentPane.add(resultsPanel, BorderLayout.CENTER); contentPane.add(buttonPane, BorderLayout.PAGE_END); - getRootPane().setDefaultButton(openBtn); searchField.addKeyListener(new KeyAdapter() { @Override public void keyReleased(KeyEvent e) { if (e.getKeyCode() == KeyEvent.VK_ENTER) { - resultsList.requestFocus(); - if (!resultsModel.isEmpty()) { - resultsList.setSelectedIndex(0); + if (resultsModel.getRowCount() != 0) { + resultsTable.setRowSelectionInterval(0, 0); } + resultsTable.requestFocus(); } } }); - KeyStroke stroke = KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0); - getRootPane().registerKeyboardAction(new ActionListener() { - @Override - public void actionPerformed(ActionEvent e) { - dispose(); - } - }, stroke, JComponent.WHEN_IN_FOCUSED_WINDOW); - setTitle(NLS.str("menu.text_search")); pack(); - setSize(700, 500); + setSize(800, 500); setLocationRelativeTo(null); setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE); setModalityType(ModalityType.MODELESS); @@ -375,4 +193,14 @@ public class SearchDialog extends JDialog { }); return chBox; } + + @Override + protected void loadFinished() { + searchField.setEnabled(true); + } + + @Override + protected void loadStart() { + searchField.setEnabled(false); + } } diff --git a/jadx-gui/src/main/java/jadx/gui/ui/TabbedPane.java b/jadx-gui/src/main/java/jadx/gui/ui/TabbedPane.java index 02e139485..52215b9d2 100644 --- a/jadx-gui/src/main/java/jadx/gui/ui/TabbedPane.java +++ b/jadx-gui/src/main/java/jadx/gui/ui/TabbedPane.java @@ -16,6 +16,7 @@ import javax.swing.JPopupMenu; import javax.swing.JTabbedPane; import javax.swing.SwingUtilities; import javax.swing.plaf.basic.BasicButtonUI; +import javax.swing.text.BadLocationException; import java.awt.Component; import java.awt.FlowLayout; import java.awt.event.ActionEvent; @@ -29,8 +30,13 @@ import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import org.jetbrains.annotations.Nullable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + class TabbedPane extends JTabbedPane { + private static final Logger LOG = LoggerFactory.getLogger(TabbedPane.class); private static final long serialVersionUID = -8833600618794570904L; private static final ImageIcon ICON_CLOSE = Utils.openIcon("cross"); @@ -65,19 +71,46 @@ class TabbedPane extends JTabbedPane { return mainWindow; } - void showCode(final Position pos) { + private void showCode(final Position pos) { final ContentPanel contentPanel = getCodePanel(pos.getNode()); SwingUtilities.invokeLater(new Runnable() { @Override public void run() { setSelectedComponent(contentPanel); ContentArea contentArea = contentPanel.getContentArea(); - contentArea.scrollToLine(pos.getLine()); + int line = pos.getLine(); + if (line < 0) { + try { + line = 1 + contentArea.getLineOfOffset(-line); + } catch (BadLocationException e) { + LOG.error("Can't get line for: {}", pos, e); + line = pos.getNode().getLine(); + } + } + contentArea.scrollToLine(line); contentArea.requestFocus(); } }); } + public void codeJump(Position pos) { + Position curPos = getCurrentPosition(); + if (curPos != null) { + jumps.addPosition(curPos); + jumps.addPosition(pos); + } + showCode(pos); + } + + @Nullable + private Position getCurrentPosition() { + ContentPanel selectedCodePanel = getSelectedCodePanel(); + if (selectedCodePanel == null) { + return null; + } + return selectedCodePanel.getContentArea().getCurrentPosition(); + } + public void navBack() { Position pos = jumps.getPrev(); if (pos != null) { @@ -116,6 +149,7 @@ class TabbedPane extends JTabbedPane { return panel; } + @Nullable ContentPanel getSelectedCodePanel() { return (ContentPanel) getSelectedComponent(); } diff --git a/jadx-gui/src/main/java/jadx/gui/ui/UsageDialog.java b/jadx-gui/src/main/java/jadx/gui/ui/UsageDialog.java new file mode 100644 index 000000000..a558b7cb5 --- /dev/null +++ b/jadx-gui/src/main/java/jadx/gui/ui/UsageDialog.java @@ -0,0 +1,101 @@ +package jadx.gui.ui; + +import jadx.gui.treemodel.JNode; +import jadx.gui.utils.CodeUsageInfo; +import jadx.gui.utils.NLS; + +import javax.swing.BorderFactory; +import javax.swing.JLabel; +import javax.swing.JPanel; +import javax.swing.SwingConstants; +import javax.swing.SwingUtilities; +import javax.swing.WindowConstants; +import java.awt.BorderLayout; +import java.awt.Container; +import java.awt.FlowLayout; +import java.awt.event.WindowAdapter; +import java.awt.event.WindowEvent; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class UsageDialog extends CommonSearchDialog { + + private static final long serialVersionUID = -5105405789969134105L; + + private static final Logger LOG = LoggerFactory.getLogger(UsageDialog.class); + + private final JNode node; + + public UsageDialog(MainWindow mainWindow, JNode node) { + super(mainWindow); + this.node = node; + + initUI(); + addWindowListener(new WindowAdapter() { + @Override + public void windowOpened(WindowEvent e) { + SwingUtilities.invokeLater(new Runnable() { + @Override + public void run() { + openInit(); + } + }); + } + }); + } + + protected void openInit() { + prepare(); + } + + @Override + protected void loadFinished() { + performSearch(); + } + + @Override + protected void loadStart() { + } + + private synchronized void performSearch() { + resultsModel.clear(); + + CodeUsageInfo usageInfo = cache.getUsageInfo(); + if (usageInfo == null) { + return; + } + resultsModel.addAll(usageInfo.getUsageList(node)); + // TODO: highlight only needed node usage + highlightText = null; + resultsTable.updateTable(); + } + + private void initUI() { + JLabel lbl = new JLabel(NLS.str("usage_dialog.label")); + JLabel nodeLabel = new JLabel(this.node.makeLongString(), this.node.getIcon(), SwingConstants.LEFT); + lbl.setLabelFor(nodeLabel); + + JPanel searchPane = new JPanel(); + searchPane.setLayout(new FlowLayout(FlowLayout.LEFT)); + searchPane.add(lbl); + searchPane.add(nodeLabel); + searchPane.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10)); + + initCommon(); + 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); + + setTitle(NLS.str("usage_dialog.title")); + pack(); + setSize(800, 500); + setLocationRelativeTo(null); + setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE); + setModalityType(ModalityType.MODELESS); + } +} diff --git a/jadx-gui/src/main/java/jadx/gui/utils/CacheObject.java b/jadx-gui/src/main/java/jadx/gui/utils/CacheObject.java index aec65dec9..63f00a33f 100644 --- a/jadx-gui/src/main/java/jadx/gui/utils/CacheObject.java +++ b/jadx-gui/src/main/java/jadx/gui/utils/CacheObject.java @@ -1,13 +1,31 @@ package jadx.gui.utils; +import jadx.gui.jobs.DecompileJob; +import jadx.gui.jobs.IndexJob; + import org.jetbrains.annotations.Nullable; public class CacheObject { - @Nullable + + private DecompileJob decompileJob; + private IndexJob indexJob; + private TextSearchIndex textIndex; + private CodeUsageInfo usageInfo; + private String lastSearch; public void reset() { textIndex = null; + lastSearch = null; + usageInfo = null; + } + + public DecompileJob getDecompileJob() { + return decompileJob; + } + + public void setDecompileJob(DecompileJob decompileJob) { + this.decompileJob = decompileJob; } @Nullable @@ -15,7 +33,33 @@ public class CacheObject { return textIndex; } - public void setTextIndex(@Nullable TextSearchIndex textIndex) { + public void setTextIndex(TextSearchIndex textIndex) { this.textIndex = textIndex; } + + @Nullable + public String getLastSearch() { + return lastSearch; + } + + public void setLastSearch(String lastSearch) { + this.lastSearch = lastSearch; + } + + @Nullable + public CodeUsageInfo getUsageInfo() { + return usageInfo; + } + + public void setUsageInfo(@Nullable CodeUsageInfo usageInfo) { + this.usageInfo = usageInfo; + } + + public IndexJob getIndexJob() { + return indexJob; + } + + public void setIndexJob(IndexJob indexJob) { + this.indexJob = indexJob; + } } diff --git a/jadx-gui/src/main/java/jadx/gui/utils/CodeLinesInfo.java b/jadx-gui/src/main/java/jadx/gui/utils/CodeLinesInfo.java new file mode 100644 index 000000000..b2c8176af --- /dev/null +++ b/jadx-gui/src/main/java/jadx/gui/utils/CodeLinesInfo.java @@ -0,0 +1,36 @@ +package jadx.gui.utils; + +import jadx.api.JavaClass; +import jadx.api.JavaMethod; +import jadx.api.JavaNode; + +import java.util.Map; +import java.util.NavigableMap; +import java.util.TreeMap; + +public class CodeLinesInfo { + private NavigableMap map = new TreeMap(); + + public CodeLinesInfo(JavaClass cls) { + addClass(cls); + } + + public void addClass(JavaClass cls) { + map.put(cls.getDecompiledLine(), cls); + for (JavaClass innerCls : cls.getInnerClasses()) { + map.put(innerCls.getDecompiledLine(), innerCls); + addClass(innerCls); + } + for (JavaMethod mth : cls.getMethods()) { + map.put(mth.getDecompiledLine(), mth); + } + } + + public JavaNode getJavaNodeByLine(int line) { + Map.Entry entry = map.floorEntry(line); + if (entry == null) { + return null; + } + return entry.getValue(); + } +} diff --git a/jadx-gui/src/main/java/jadx/gui/utils/CodeUsageInfo.java b/jadx-gui/src/main/java/jadx/gui/utils/CodeUsageInfo.java new file mode 100644 index 000000000..29a665141 --- /dev/null +++ b/jadx-gui/src/main/java/jadx/gui/utils/CodeUsageInfo.java @@ -0,0 +1,57 @@ +package jadx.gui.utils; + +import jadx.api.CodePosition; +import jadx.api.JavaClass; +import jadx.api.JavaNode; +import jadx.gui.treemodel.CodeNode; +import jadx.gui.treemodel.JNode; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class CodeUsageInfo { + + public static class UsageInfo { + private final List usageList = new ArrayList(); + + public List getUsageList() { + return usageList; + } + } + + private final Map usageMap = new HashMap(); + + public void processClass(JavaClass javaClass, CodeLinesInfo linesInfo, String[] lines) { + Map usage = javaClass.getUsageMap(); + for (Map.Entry entry : usage.entrySet()) { + CodePosition codePosition = entry.getKey(); + JavaNode javaNode = entry.getValue(); + addUsage(JNode.makeFrom(javaNode), javaClass, linesInfo, codePosition, lines); + } + } + + private void addUsage(JNode jNode, JavaClass javaClass, + CodeLinesInfo linesInfo, CodePosition codePosition, String[] lines) { + UsageInfo usageInfo = usageMap.get(jNode); + if (usageInfo == null) { + usageInfo = new UsageInfo(); + usageMap.put(jNode, usageInfo); + } + int line = codePosition.getLine(); + JavaNode javaNodeByLine = linesInfo.getJavaNodeByLine(line); + String codeLine = lines[line - 1].trim(); + CodeNode codeNode = new CodeNode(javaNodeByLine == null ? javaClass : javaNodeByLine, line, codeLine); + usageInfo.getUsageList().add(codeNode); + } + + public List getUsageList(JNode node) { + UsageInfo usageInfo = usageMap.get(node); + if (usageInfo == null) { + return Collections.emptyList(); + } + return usageInfo.getUsageList(); + } +} diff --git a/jadx-gui/src/main/java/jadx/gui/utils/LogCollector.java b/jadx-gui/src/main/java/jadx/gui/utils/LogCollector.java index cbacbb148..ff94ea1b7 100644 --- a/jadx-gui/src/main/java/jadx/gui/utils/LogCollector.java +++ b/jadx-gui/src/main/java/jadx/gui/utils/LogCollector.java @@ -49,7 +49,7 @@ public class LogCollector extends CyclicBufferAppender { public LogCollector() { setName("LogCollector"); - setMaxSize(50000); + setMaxSize(5000); } @Override diff --git a/jadx-gui/src/main/java/jadx/gui/utils/SuffixTree.java b/jadx-gui/src/main/java/jadx/gui/utils/SuffixTree.java new file mode 100644 index 000000000..d5caf651b --- /dev/null +++ b/jadx-gui/src/main/java/jadx/gui/utils/SuffixTree.java @@ -0,0 +1,28 @@ +package jadx.gui.utils; + +import com.googlecode.concurrenttrees.radix.node.concrete.DefaultCharArrayNodeFactory; +import com.googlecode.concurrenttrees.suffix.ConcurrentSuffixTree; + +public class SuffixTree { + + private final ConcurrentSuffixTree tree; + + public SuffixTree() { + this.tree = new ConcurrentSuffixTree(new DefaultCharArrayNodeFactory()); + } + + public void put(String str, V value) { + if (str == null || str.isEmpty()) { + return; + } + tree.putIfAbsent(str, value); + } + + public Iterable getValuesForKeysContaining(String str) { + return tree.getValuesForKeysContaining(str); + } + + public int size() { + return tree.size(); + } +} diff --git a/jadx-gui/src/main/java/jadx/gui/utils/TextSearchIndex.java b/jadx-gui/src/main/java/jadx/gui/utils/TextSearchIndex.java index c56eabe6c..d4b7278b7 100644 --- a/jadx-gui/src/main/java/jadx/gui/utils/TextSearchIndex.java +++ b/jadx-gui/src/main/java/jadx/gui/utils/TextSearchIndex.java @@ -3,19 +3,18 @@ package jadx.gui.utils; import jadx.api.JavaClass; import jadx.api.JavaField; import jadx.api.JavaMethod; +import jadx.api.JavaNode; +import jadx.core.codegen.CodeWriter; import jadx.gui.treemodel.CodeNode; import jadx.gui.treemodel.JNode; +import jadx.gui.ui.CommonSearchDialog; -import java.io.BufferedReader; -import java.io.StringReader; +import java.util.ArrayList; +import java.util.List; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import com.googlecode.concurrenttrees.radix.node.concrete.DefaultCharArrayNodeFactory; -import com.googlecode.concurrenttrees.suffix.ConcurrentSuffixTree; -import com.googlecode.concurrenttrees.suffix.SuffixTree; - public class TextSearchIndex { private static final Logger LOG = LoggerFactory.getLogger(TextSearchIndex.class); @@ -25,15 +24,16 @@ public class TextSearchIndex { private SuffixTree fldNamesTree; private SuffixTree codeTree; + private List skippedClasses = new ArrayList(); + public TextSearchIndex() { - clsNamesTree = new ConcurrentSuffixTree(new DefaultCharArrayNodeFactory()); - mthNamesTree = new ConcurrentSuffixTree(new DefaultCharArrayNodeFactory()); - fldNamesTree = new ConcurrentSuffixTree(new DefaultCharArrayNodeFactory()); - codeTree = new ConcurrentSuffixTree(new DefaultCharArrayNodeFactory()); + clsNamesTree = new SuffixTree(); + mthNamesTree = new SuffixTree(); + fldNamesTree = new SuffixTree(); + codeTree = new SuffixTree(); } public void indexNames(JavaClass cls) { - cls.decompile(); clsNamesTree.put(cls.getFullName(), JNode.makeFrom(cls)); for (JavaMethod mth : cls.getMethods()) { mthNamesTree.put(mth.getFullName(), JNode.makeFrom(mth)); @@ -46,18 +46,15 @@ public class TextSearchIndex { } } - public void indexCode(JavaClass cls) { + public void indexCode(JavaClass cls, CodeLinesInfo linesInfo, String[] lines) { try { - String code = cls.getCode(); - BufferedReader bufReader = new BufferedReader(new StringReader(code)); - String line; - int lineNum = 0; - while ((line = bufReader.readLine()) != null) { - lineNum++; - line = line.trim(); + int count = lines.length; + for (int i = 0; i < count; i++) { + String line = lines[i]; if (!line.isEmpty()) { - CodeNode node = new CodeNode(cls, lineNum, line); - codeTree.put(line, node); + int lineNum = i + 1; + JavaNode node = linesInfo.getJavaNodeByLine(lineNum); + codeTree.put(line, new CodeNode(node == null ? cls : node, lineNum, line)); } } } catch (Exception e) { @@ -78,6 +75,59 @@ public class TextSearchIndex { } public Iterable searchCode(String text) { - return codeTree.getValuesForKeysContaining(text); + Iterable items; + if (codeTree.size() > 0) { + items = codeTree.getValuesForKeysContaining(text); + if (skippedClasses.isEmpty()) { + return items; + } + } else { + items = null; + } + List list = new ArrayList(); + if (items != null) { + for (CodeNode item : items) { + list.add(item); + } + } + addSkippedClasses(list, text); + return list; + } + + private void addSkippedClasses(List list, String text) { + for (JavaClass javaClass : skippedClasses) { + String code = javaClass.getCode(); + int pos = 0; + while (pos != -1) { + pos = searchNext(list, text, javaClass, code, pos); + } + if (list.size() > CommonSearchDialog.MAX_RESULTS_COUNT) { + return; + } + } + } + + private int searchNext(List list, String text, JavaNode javaClass, String code, int startPos) { + int pos = code.indexOf(text, startPos); + if (pos == -1) { + return -1; + } + int lineStart = 1 + code.lastIndexOf(CodeWriter.NL, pos); + int lineEnd = code.indexOf(CodeWriter.NL, pos + text.length()); + String line = code.substring(lineStart, lineEnd == -1 ? code.length() : lineEnd); + list.add(new CodeNode(javaClass, -pos, line.trim())); + return lineEnd; + } + + public void classCodeIndexSkipped(JavaClass cls) { + this.skippedClasses.add(cls); + } + + public List getSkippedClasses() { + return skippedClasses; + } + + public int getSkippedCount() { + return skippedClasses.size(); } } diff --git a/jadx-gui/src/main/java/jadx/gui/utils/Utils.java b/jadx-gui/src/main/java/jadx/gui/utils/Utils.java index 8ffbd5a95..c51aa64d7 100644 --- a/jadx-gui/src/main/java/jadx/gui/utils/Utils.java +++ b/jadx-gui/src/main/java/jadx/gui/utils/Utils.java @@ -84,4 +84,30 @@ public class Utils { } return overIcon; } + + public static boolean isFreeMemoryAvailable() { + Runtime runtime = Runtime.getRuntime(); + long maxMemory = runtime.maxMemory(); + long totalFree = runtime.freeMemory() + maxMemory - runtime.totalMemory(); + return totalFree > maxMemory * 0.2; + } + + public static String memoryInfo() { + Runtime runtime = Runtime.getRuntime(); + StringBuilder sb = new StringBuilder(); + long maxMemory = runtime.maxMemory(); + long allocatedMemory = runtime.totalMemory(); + long freeMemory = runtime.freeMemory(); + + sb.append("free: ").append(format(freeMemory)); + sb.append(", allocated: ").append(format(allocatedMemory)); + sb.append(", max: ").append(format(maxMemory)); + sb.append(", total free: ").append(format(freeMemory + maxMemory - allocatedMemory)); + + return sb.toString(); + } + + private static String format(long mem) { + return Long.toString((long) (mem / 1024. / 1024.)) + "MB"; + } } diff --git a/jadx-gui/src/main/resources/i18n/Messages_en_US.properties b/jadx-gui/src/main/resources/i18n/Messages_en_US.properties index f76419885..a6905633a 100644 --- a/jadx-gui/src/main/resources/i18n/Messages_en_US.properties +++ b/jadx-gui/src/main/resources/i18n/Messages_en_US.properties @@ -47,6 +47,9 @@ search_dialog.method=Method search_dialog.field=Field search_dialog.code=Code +usage_dialog.title=Usage search +usage_dialog.label=Usage for: + preferences.title=Preferences preferences.deobfuscation=Deobfuscation preferences.other=Other @@ -58,6 +61,7 @@ preferences.threads=Processing threads count preferences.cfg=Generate methods CFG graphs (in 'dot' format) preferences.raw_cfg=Generate RAW CFG graphs preferences.font=Editor font +preferences.fast_search=Fast search (uses more memory) preferences.select_font=Select preferences.deobfuscation_on=Enable deobfuscation preferences.deobfuscation_force=Force rewrite deobfuscation map file