feat(gui): add issues panel and summary report (#986)

This commit is contained in:
Skylot
2021-10-23 16:03:06 +01:00
parent 439446816c
commit 82712776cc
17 changed files with 452 additions and 18 deletions
@@ -83,7 +83,7 @@ public class Deobfuscator {
public void savePresets() {
Path deobfMapFile = deobfPresets.getDeobfMapFile();
if (Files.exists(deobfMapFile) && !args.isDeobfuscationForceSave()) {
LOG.warn("Deobfuscation map file '{}' exists. Use command line option '--deobf-rewrite-cfg' to rewrite it",
LOG.info("Deobfuscation map file '{}' exists. Use command line option '--deobf-rewrite-cfg' to rewrite it",
deobfMapFile.toAbsolutePath());
return;
}
@@ -80,6 +80,8 @@ import org.jetbrains.annotations.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import ch.qos.logback.classic.Level;
import jadx.api.JadxArgs;
import jadx.api.JavaClass;
import jadx.api.JavaNode;
@@ -114,9 +116,11 @@ import jadx.gui.ui.dialog.LogViewerDialog;
import jadx.gui.ui.dialog.RenameDialog;
import jadx.gui.ui.dialog.SearchDialog;
import jadx.gui.ui.panel.ContentPanel;
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.SummaryNode;
import jadx.gui.update.JadxUpdate;
import jadx.gui.update.JadxUpdate.IUpdateCallback;
import jadx.gui.update.data.Release;
@@ -129,6 +133,7 @@ import jadx.gui.utils.Link;
import jadx.gui.utils.NLS;
import jadx.gui.utils.SystemInfo;
import jadx.gui.utils.UiUtils;
import jadx.gui.utils.logs.LogCollector;
import jadx.gui.utils.search.CommentsIndex;
import jadx.gui.utils.search.TextSearchIndex;
@@ -402,6 +407,7 @@ public class MainWindow extends JFrame {
project.setFilePath(paths);
clearTree();
BreakpointManager.saveAndExit();
LogCollector.getInstance().reset();
if (paths.isEmpty()) {
return;
}
@@ -413,11 +419,31 @@ public class MainWindow extends JFrame {
UiUtils.errorMessage(this, NLS.str("message.memoryLow"));
return;
}
checkLoadedStatus();
onOpen(paths);
onFinish.run();
});
}
private void checkLoadedStatus() {
if (!wrapper.getClasses().isEmpty()) {
return;
}
int errors = LogCollector.getInstance().getErrors();
if (errors > 0) {
int result = JOptionPane.showConfirmDialog(this,
NLS.str("message.load_errors", errors),
NLS.str("message.errorTitle"),
JOptionPane.OK_CANCEL_OPTION,
JOptionPane.ERROR_MESSAGE);
if (result == JOptionPane.OK_OPTION) {
LogViewerDialog.openWithLevel(this, Level.ERROR);
}
} else {
UiUtils.showMessageBox(this, NLS.str("message.no_classes"));
}
}
private void onOpen(List<Path> paths) {
deobfToggleBtn.setSelected(settings.isDeobfuscationOn());
initTree();
@@ -428,6 +454,7 @@ public class MainWindow extends JFrame {
private void addTreeCustomNodes() {
treeRoot.replaceCustomNode(ApkSignature.getApkSignature(wrapper));
treeRoot.replaceCustomNode(new SummaryNode(this));
}
private boolean ensureProjectIsSaved() {
@@ -723,7 +750,7 @@ public class MainWindow extends JFrame {
}
private void treeRightClickAction(MouseEvent e) {
JNode obj = getJNodeUnderMouse(e);
JNode obj = getJNodeUnderMouse(e, false);
if (obj instanceof JPackage) {
JPackagePopupMenu menu = new JPackagePopupMenu(this, (JPackage) obj);
menu.show(e.getComponent(), e.getX(), e.getY());
@@ -737,8 +764,12 @@ public class MainWindow extends JFrame {
}
@Nullable
private JNode getJNodeUnderMouse(MouseEvent mouseEvent) {
private JNode getJNodeUnderMouse(MouseEvent mouseEvent, boolean trySelection) {
TreePath path = tree.getPathForLocation(mouseEvent.getX(), mouseEvent.getY());
if (path == null && trySelection) {
// maybe click on node row (mouse pressed event should select this node in tree)
path = tree.getSelectionPath();
}
if (path != null) {
Object obj = path.getLastPathComponent();
if (obj instanceof JNode) {
@@ -939,7 +970,7 @@ public class MainWindow extends JFrame {
Action logAction = new AbstractAction(NLS.str("menu.log"), ICON_LOG) {
@Override
public void actionPerformed(ActionEvent e) {
new LogViewerDialog(MainWindow.this).setVisible(true);
LogViewerDialog.open(MainWindow.this);
}
};
logAction.putValue(Action.SHORT_DESCRIPTION, NLS.str("menu.log"));
@@ -1096,10 +1127,16 @@ public class MainWindow extends JFrame {
ToolTipManager.sharedInstance().registerComponent(tree);
tree.getSelectionModel().setSelectionMode(TreeSelectionModel.SINGLE_TREE_SELECTION);
tree.addMouseListener(new MouseAdapter() {
@Override
public void mousePressed(MouseEvent e) {
super.mousePressed(e);
}
@Override
public void mouseClicked(MouseEvent e) {
if (SwingUtilities.isLeftMouseButton(e)) {
nodeClickAction(getJNodeUnderMouse(e));
nodeClickAction(getJNodeUnderMouse(e, true));
} else if (SwingUtilities.isRightMouseButton(e)) {
treeRightClickAction(e);
}
@@ -1157,13 +1194,18 @@ public class MainWindow extends JFrame {
});
progressPane = new ProgressPanel(this, true);
IssuesPanel issuesPanel = new IssuesPanel(this);
JPanel leftPane = new JPanel(new BorderLayout());
JScrollPane treeScrollPane = new JScrollPane(tree);
treeScrollPane.setMinimumSize(new Dimension(100, 150));
JPanel bottomPane = new JPanel(new BorderLayout());
bottomPane.add(issuesPanel, BorderLayout.PAGE_START);
bottomPane.add(progressPane, BorderLayout.PAGE_END);
leftPane.add(treeScrollPane, BorderLayout.CENTER);
leftPane.add(progressPane, BorderLayout.PAGE_END);
leftPane.add(bottomPane, BorderLayout.PAGE_END);
splitPane.setLeftComponent(leftPane);
tabbedPane = new TabbedPane(this);
@@ -32,7 +32,16 @@ public class LogViewerDialog extends JDialog {
private final transient JadxSettings settings;
private transient RSyntaxTextArea textPane;
public LogViewerDialog(MainWindow mainWindow) {
public static void open(MainWindow mainWindow) {
openWithLevel(mainWindow, level);
}
public static void openWithLevel(MainWindow mainWindow, Level newLevel) {
level = newLevel;
new LogViewerDialog(mainWindow).setVisible(true);
}
private LogViewerDialog(MainWindow mainWindow) {
this.settings = mainWindow.getSettings();
initUI(mainWindow);
registerLogListener();
@@ -0,0 +1,83 @@
package jadx.gui.ui.panel;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import javax.swing.BorderFactory;
import javax.swing.Box;
import javax.swing.BoxLayout;
import javax.swing.ImageIcon;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.SwingUtilities;
import ch.qos.logback.classic.Level;
import jadx.gui.ui.MainWindow;
import jadx.gui.ui.dialog.LogViewerDialog;
import jadx.gui.utils.NLS;
import jadx.gui.utils.UiUtils;
import jadx.gui.utils.logs.LogCollector;
public class IssuesPanel extends JPanel {
private static final long serialVersionUID = -7720576036668459218L;
private static final ImageIcon ERROR_ICON = UiUtils.openSvgIcon("ui/error");
private static final ImageIcon WARN_ICON = UiUtils.openSvgIcon("ui/warning");
private final MainWindow mainWindow;
private JLabel errorLabel;
private JLabel warnLabel;
public IssuesPanel(MainWindow mainWindow) {
this.mainWindow = mainWindow;
initUI();
LogCollector.getInstance().registerIssueListener((error, warnings) -> {
SwingUtilities.invokeLater(() -> onUpdate(error, warnings));
});
}
private void initUI() {
JLabel label = new JLabel(NLS.str("issues_panel.label"));
errorLabel = new JLabel(ERROR_ICON);
warnLabel = new JLabel(WARN_ICON);
String toolTipText = NLS.str("issues_panel.tooltip");
errorLabel.setToolTipText(toolTipText);
warnLabel.setToolTipText(toolTipText);
errorLabel.addMouseListener(new MouseAdapter() {
@Override
public void mouseClicked(MouseEvent e) {
LogViewerDialog.openWithLevel(mainWindow, Level.ERROR);
}
});
warnLabel.addMouseListener(new MouseAdapter() {
@Override
public void mouseClicked(MouseEvent e) {
LogViewerDialog.openWithLevel(mainWindow, Level.WARN);
}
});
setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5));
setLayout(new BoxLayout(this, BoxLayout.X_AXIS));
setVisible(false);
add(label);
add(Box.createHorizontalGlue());
add(errorLabel);
add(Box.createHorizontalGlue());
add(warnLabel);
}
private void onUpdate(int error, int warnings) {
if (error == 0 && warnings == 0) {
setVisible(false);
return;
}
setVisible(true);
errorLabel.setText(NLS.str("issues_panel.errors", error));
errorLabel.setVisible(error != 0);
warnLabel.setText(NLS.str("issues_panel.warnings", warnings));
warnLabel.setVisible(warnings != 0);
}
}
@@ -0,0 +1,163 @@
package jadx.gui.ui.treenodes;
import java.io.File;
import java.io.IOException;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
import javax.swing.Icon;
import javax.swing.ImageIcon;
import org.apache.commons.text.StringEscapeUtils;
import jadx.api.JadxDecompiler;
import jadx.core.dex.attributes.IAttributeNode;
import jadx.core.dex.nodes.ClassNode;
import jadx.core.dex.nodes.MethodNode;
import jadx.core.dex.nodes.ProcessState;
import jadx.core.utils.ErrorsCounter;
import jadx.core.utils.Utils;
import jadx.gui.treemodel.JClass;
import jadx.gui.treemodel.JNode;
import jadx.gui.ui.MainWindow;
import jadx.gui.ui.TabbedPane;
import jadx.gui.ui.panel.ContentPanel;
import jadx.gui.ui.panel.HtmlPanel;
import jadx.gui.utils.UiUtils;
public class SummaryNode extends JNode {
private static final long serialVersionUID = 4295299814582784805L;
private static final ImageIcon ICON = UiUtils.openSvgIcon("nodes/detailView");
private final MainWindow mainWindow;
public SummaryNode(MainWindow mainWindow) {
this.mainWindow = mainWindow;
}
@Override
public String getContent() {
StringEscapeUtils.Builder builder = StringEscapeUtils.builder(StringEscapeUtils.ESCAPE_HTML4);
try {
builder.append("<html>");
builder.append("<body>");
writeInputSummary(builder);
writeDecompilationSummary(builder);
builder.append("</body>");
} catch (Exception e) {
builder.append("Error build summary: ");
builder.append("<pre>");
builder.append(Utils.getStackTrace(e));
builder.append("</pre>");
}
return builder.toString();
}
private void writeInputSummary(StringEscapeUtils.Builder builder) throws IOException {
builder.append("<h2>Input</h2>");
JadxDecompiler jadx = mainWindow.getWrapper().getDecompiler();
builder.append("<h3>Files</h3>");
builder.append("<ul>");
for (File inputFile : jadx.getArgs().getInputFiles()) {
builder.append("<li>");
builder.escape(inputFile.getCanonicalFile().getAbsolutePath());
builder.append("</li>");
}
builder.append("</ul>");
List<ClassNode> classes = jadx.getRoot().getClasses(true);
List<String> codeSources = classes.stream()
.map(ClassNode::getInputFileName)
.distinct()
.sorted()
.collect(Collectors.toList());
codeSources.remove("synthetic");
int codeSourcesCount = codeSources.size();
builder.append("<h3>Code sources</h3>");
builder.append("<ul>");
if (codeSourcesCount != 1) {
builder.append("<li>Count: " + codeSourcesCount + "</li>");
}
// dex files list
codeSources.removeIf(f -> !f.endsWith(".dex"));
if (!codeSources.isEmpty()) {
for (String input : codeSources) {
builder.append("<li>");
builder.escape(input);
builder.append("</li>");
}
}
builder.append("</ul>");
int methodsCount = classes.stream().mapToInt(cls -> cls.getMethods().size()).sum();
int fieldsCount = classes.stream().mapToInt(cls -> cls.getFields().size()).sum();
int insnCount = classes.stream().flatMap(cls -> cls.getMethods().stream()).mapToInt(MethodNode::getInsnsCount).sum();
builder.append("<h3>Counts</h3>");
builder.append("<ul>");
builder.append("<li>Classes: " + classes.size() + "</li>");
builder.append("<li>Methods: " + methodsCount + "</li>");
builder.append("<li>Fields: " + fieldsCount + "</li>");
builder.append("<li>Instructions: " + insnCount + " (units)</li>");
builder.append("</ul>");
}
private void writeDecompilationSummary(StringEscapeUtils.Builder builder) {
builder.append("<h2>Decompilation</h2>");
JadxDecompiler jadx = mainWindow.getWrapper().getDecompiler();
List<ClassNode> classes = jadx.getRoot().getClasses(false);
int classesCount = classes.size();
long processedClasses = classes.stream().filter(c -> c.getState() == ProcessState.PROCESS_COMPLETE).count();
long generatedClasses = classes.stream().filter(c -> c.getState() == ProcessState.GENERATED_AND_UNLOADED).count();
builder.append("<ul>");
builder.append("<li>Top level classes: " + classesCount + "</li>");
builder.append("<li>At process stage: " + valueAndPercent(processedClasses, classesCount) + "</li>");
builder.append("<li>Code generated: " + valueAndPercent(generatedClasses, classesCount) + "</li>");
builder.append("</ul>");
ErrorsCounter counter = jadx.getRoot().getErrorsCounter();
Set<IAttributeNode> problemNodes = new HashSet<>();
problemNodes.addAll(counter.getErrorNodes());
problemNodes.addAll(counter.getWarnNodes());
long problemMethods = problemNodes.stream().filter(MethodNode.class::isInstance).count();
int methodsCount = classes.stream().mapToInt(cls -> cls.getMethods().size()).sum();
double methodSuccessRate = (methodsCount - problemMethods) * 100.0 / (double) methodsCount;
builder.append("<h3>Issues</h3>");
builder.append("<ul>");
builder.append("<li>Errors: " + counter.getErrorCount() + "</li>");
builder.append("<li>Warnings: " + counter.getWarnsCount() + "</li>");
builder.append("<li>Nodes with errors: " + counter.getErrorNodes().size() + "</li>");
builder.append("<li>Nodes with warnings: " + counter.getWarnNodes().size() + "</li>");
builder.append("<li>Total nodes with issues: " + problemNodes.size() + "</li>");
builder.append("<li>Methods with issues: " + problemMethods + "</li>");
builder.append("<li>Methods success rate: " + String.format("%.2f", methodSuccessRate) + "%</li>");
builder.append("</ul>");
}
private String valueAndPercent(long value, int total) {
return String.format("%d (%.2f%%)", value, value * 100 / ((double) total));
}
@Override
public ContentPanel getContentPanel(TabbedPane tabbedPane) {
return new HtmlPanel(tabbedPane, this);
}
@Override
public String makeString() {
return "Summary";
}
@Override
public Icon getIcon() {
return ICON;
}
@Override
public JClass getJParent() {
return null;
}
}
@@ -253,10 +253,6 @@ public class UiUtils {
return CTRL_BNT_KEY;
}
public static void showMessageBox(Component parent, String msg) {
JOptionPane.showMessageDialog(parent, msg);
}
public static void addEscapeShortCutToDispose(JDialog dialog) {
KeyStroke stroke = KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0);
dialog.getRootPane().registerKeyboardAction(e -> dialog.dispose(), stroke, JComponent.WHEN_IN_FOCUSED_WINDOW);
@@ -292,6 +288,10 @@ public class UiUtils {
return envVal;
}
public static void showMessageBox(Component parent, String msg) {
JOptionPane.showMessageDialog(parent, msg);
}
public static void errorMessage(Component parent, String message) {
JOptionPane.showMessageDialog(parent, message,
NLS.str("message.errorTitle"), JOptionPane.ERROR_MESSAGE);
@@ -0,0 +1,5 @@
package jadx.gui.utils.logs;
public interface ILogIssuesListener {
void onChange(int error, int warnings);
}
@@ -0,0 +1,50 @@
package jadx.gui.utils.logs;
import java.util.AbstractQueue;
import java.util.ArrayDeque;
import java.util.Deque;
import java.util.Iterator;
public class LimitedQueue<T> extends AbstractQueue<T> {
private final Deque<T> deque = new ArrayDeque<>();
private final int limit;
public LimitedQueue(int limit) {
this.limit = limit;
}
@Override
public Iterator<T> iterator() {
return deque.iterator();
}
@Override
public int size() {
return deque.size();
}
@Override
public boolean offer(T t) {
deque.addLast(t);
if (deque.size() > limit) {
deque.removeFirst();
}
return true;
}
@Override
public T poll() {
return deque.poll();
}
@Override
public T peek() {
return deque.peek();
}
@Override
public void clear() {
deque.clear();
}
}
@@ -1,7 +1,6 @@
package jadx.gui.utils.logs;
import java.util.Deque;
import java.util.LinkedList;
import java.util.Queue;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@@ -44,8 +43,12 @@ public class LogCollector extends AppenderBase<ILoggingEvent> {
@Nullable
private ILogListener listener;
@Nullable
private ILogIssuesListener issuesListener;
private int errors = 0;
private int warnings = 0;
private final Deque<LogEvent> buffer = new LinkedList<>();
private final Queue<LogEvent> buffer = new LimitedQueue<>(BUFFER_SIZE);
public LogCollector() {
setName("LogCollector");
@@ -60,14 +63,24 @@ public class LogCollector extends AppenderBase<ILoggingEvent> {
if (listener != null && level.isGreaterOrEqual(listener.getFilterLevel())) {
listener.onAppend(msg);
}
if (level == Level.ERROR) {
errors++;
issuesUpdated();
} else if (level == Level.WARN) {
warnings++;
issuesUpdated();
}
}
}
private void issuesUpdated() {
if (issuesListener != null) {
issuesListener.onChange(errors, warnings);
}
}
private void store(Level level, String msg) {
buffer.addLast(new LogEvent(level, msg));
if (buffer.size() > BUFFER_SIZE) {
buffer.pollFirst();
}
buffer.offer(new LogEvent(level, msg));
}
public void setLayout(Layout<ILoggingEvent> layout) {
@@ -81,10 +94,32 @@ public class LogCollector extends AppenderBase<ILoggingEvent> {
}
}
public void registerIssueListener(@NotNull ILogIssuesListener listener) {
this.issuesListener = listener;
synchronized (this) {
listener.onChange(errors, warnings);
}
}
public void resetListener() {
this.listener = null;
}
public void reset() {
buffer.clear();
errors = 0;
warnings = 0;
issuesUpdated();
}
public int getErrors() {
return errors;
}
public int getWarnings() {
return warnings;
}
private String init(Level filterLevel) {
StringBuilder sb = new StringBuilder();
for (LogEvent event : buffer) {
@@ -64,6 +64,8 @@ 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=Load failed.\nErrors count: %d\nClick OK to open log viewer"
#message.no_classes=No classes loaded, nothing to decompile!
message.saveIncomplete=<html>Speichern unvollständig.<br> %s<br> %d Klassen oder Quellen wurden nicht gespeichert!</html>
message.indexIncomplete=<html>Index einiger Klassen übersprungen.<br> %s<br> %d Klassen wurden nicht indiziert und werden nicht in den Suchergebnissen erscheinen!</html>
@@ -241,6 +243,11 @@ apkSignature.warnings=Warnhinweise
apkSignature.exception=APK-Verifizierung fehlgeschlagen
apkSignature.unprotectedEntry=Dateien, die nicht durch eine Signatur geschützt sind. Unbefugte Änderungen an diesem JAR-Eintrag werden nicht erkannt.
#issues_panel.label=Issues:
#issues_panel.errors=%d errors
#issues_panel.warnings=%d warnings
#issues_panel.tooltip=Open in log viewer
debugger.process_selector=Zu debuggenden Prozess auswählen
debugger.step_into=Schritt Into (F7)
debugger.step_over=Schritt Over (F8)
@@ -64,6 +64,8 @@ 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.no_classes=No classes loaded, nothing to decompile!
message.saveIncomplete=<html>Save incomplete.<br> %s<br> %d classes or resources were not saved!</html>
message.indexIncomplete=<html>Index of some classes skipped.<br> %s<br> %d classes were not indexed and will not appear in search results!</html>
@@ -241,6 +243,11 @@ apkSignature.warnings=Warnings
apkSignature.exception=APK verification failed
apkSignature.unprotectedEntry=Files that are not protected by signature. Unauthorized modifications to this JAR entry will not be detected.
issues_panel.label=Issues:
issues_panel.errors=%d errors
issues_panel.warnings=%d warnings
issues_panel.tooltip=Open in log viewer
debugger.process_selector=Select a process to debug
debugger.step_into=Step Into (F7)
debugger.step_over=Step Over (F8)
@@ -64,6 +64,8 @@ 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.no_classes=No classes loaded, nothing to decompile!
#message.saveIncomplete=<html>Save incomplete.<br> %s<br> %d classes or resources were not saved!</html>
#message.indexIncomplete=<html>Index of some classes skipped.<br> %s<br> %d classes were not indexed and will not appear in search results!</html>
@@ -241,6 +243,11 @@ certificate.serialPubKeyY=Y
#apkSignature.exception=
#apkSignature.unprotectedEntry=
#issues_panel.label=Issues:
#issues_panel.errors=%d errors
#issues_panel.warnings=%d warnings
#issues_panel.tooltip=Open in log viewer
#debugger.process_selector=Select a process to debug
#debugger.step_into=Step Into (F7)
#debugger.step_over=Step Over (F8)
@@ -64,6 +64,8 @@ nav.forward=앞으로
#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.no_classes=No classes loaded, nothing to decompile!
#message.saveIncomplete=<html>Save incomplete.<br> %s<br> %d classes or resources were not saved!</html>
#message.indexIncomplete=<html>Index of some classes skipped.<br> %s<br> %d classes were not indexed and will not appear in search results!</html>
@@ -241,6 +243,11 @@ apkSignature.warnings=경고
apkSignature.exception=APK 검증 실패
apkSignature.unprotectedEntry=서명으로 보호되지 않는 파일. 이 JAR 항목에 대한 승인되지 않은 수정은 감지되지 않습니다.
#issues_panel.label=Issues:
#issues_panel.errors=%d errors
#issues_panel.warnings=%d warnings
#issues_panel.tooltip=Open in log viewer
debugger.process_selector=디버깅 할 프로세스 선택
debugger.step_into=한 단계씩 코드 실행 (F7)
debugger.step_over=프로시저 단위 실행 (F8)
@@ -64,6 +64,8 @@ nav.forward=前进
#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.no_classes=No classes loaded, nothing to decompile!
#message.saveIncomplete=<html>Save incomplete.<br> %s<br> %d classes or resources were not saved!</html>
#message.indexIncomplete=<html>Index of some classes skipped.<br> %s<br> %d classes were not indexed and will not appear in search results!</html>
@@ -241,6 +243,11 @@ apkSignature.warnings=警告
apkSignature.exception=APK 验证失败
apkSignature.unprotectedEntry=不受签名保护的文件。不会检测对此 JAR 条目的未经授权的修改。
#issues_panel.label=Issues:
#issues_panel.errors=%d errors
#issues_panel.warnings=%d warnings
#issues_panel.tooltip=Open in log viewer
#debugger.process_selector=Select a process to debug
#debugger.step_into=Step Into (F7)
#debugger.step_over=Step Over (F8)
@@ -0,0 +1,4 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M6 5H8V8H10V6H12V11H10H8H6H4V9H6V5Z" fill="#6E6E6E"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M15 1H1V15H15V1ZM13 3H3V13H13V3Z" fill="#6E6E6E"/>
</svg>

After

Width:  |  Height:  |  Size: 306 B

@@ -0,0 +1,4 @@
<!-- Copyright 2000-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. -->
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
<path fill="#E05555" fill-opacity=".7" fill-rule="evenodd" d="M8,14 C4.6862915,14 2,11.3137085 2,8 C2,4.6862915 4.6862915,2 8,2 C11.3137085,2 14,4.6862915 14,8 C14,11.3137085 11.3137085,14 8,14 Z M7,4 L7,8 L9,8 L9,4 L7,4 Z M7,10 L7,12 L9,12 L9,10 L7,10 Z"/>
</svg>

After

Width:  |  Height:  |  Size: 498 B

@@ -0,0 +1,4 @@
<!-- Copyright 2000-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. -->
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
<path fill="#F4AF3D" fill-rule="evenodd" d="M8,2 L15,14 L1,14 L8,2 Z M9,13 L9,11 L7,11 L7,13 L9,13 Z M9,10 L9,6 L7,6 L7,10 L9,10 Z"/>
</svg>

After

Width:  |  Height:  |  Size: 374 B