From b09c7ba6b838e1b82e6aaa738758a7393367a0c9 Mon Sep 17 00:00:00 2001 From: Ahmed Ashour Date: Sun, 31 Mar 2019 19:20:27 +0200 Subject: [PATCH] feat(gui): support project (#526) (PR #543) --- .../src/main/java/jadx/cli/JadxCLIArgs.java | 4 - jadx-gui/src/main/java/jadx/gui/JadxGUI.java | 2 +- .../src/main/java/jadx/gui/JadxWrapper.java | 37 +- .../java/jadx/gui/jobs/BackgroundWorker.java | 3 +- .../java/jadx/gui/settings/JadxProject.java | 127 +++++++ .../java/jadx/gui/settings/JadxSettings.java | 63 +++- .../gui/settings/JadxSettingsAdapter.java | 12 +- .../jadx/gui/settings/JadxSettingsWindow.java | 35 +- .../java/jadx/gui/treemodel/ApkSignature.java | 8 +- .../java/jadx/gui/ui/CommonSearchDialog.java | 9 +- .../main/java/jadx/gui/ui/HeapUsageBar.java | 4 +- .../main/java/jadx/gui/ui/MainDropTarget.java | 2 +- .../src/main/java/jadx/gui/ui/MainWindow.java | 325 ++++++++++++++---- .../src/main/java/jadx/gui/utils/NLS.java | 8 +- .../java/jadx/gui/utils/PathTypeAdapter.java | 44 +++ .../resources/i18n/Messages_en_US.properties | 19 +- .../resources/i18n/Messages_es_ES.properties | 19 +- .../resources/i18n/Messages_zh_CN.properties | 19 +- jadx-gui/src/test/java/jadx/gui/TestI18n.java | 63 +++- 19 files changed, 642 insertions(+), 161 deletions(-) create mode 100644 jadx-gui/src/main/java/jadx/gui/settings/JadxProject.java create mode 100644 jadx-gui/src/main/java/jadx/gui/utils/PathTypeAdapter.java diff --git a/jadx-cli/src/main/java/jadx/cli/JadxCLIArgs.java b/jadx-cli/src/main/java/jadx/cli/JadxCLIArgs.java index 54711f29c..a8b8b1511 100644 --- a/jadx-cli/src/main/java/jadx/cli/JadxCLIArgs.java +++ b/jadx-cli/src/main/java/jadx/cli/JadxCLIArgs.java @@ -231,10 +231,6 @@ public class JadxCLIArgs { return deobfuscationUseSourceNameAsAlias; } - public boolean escapeUnicode() { - return escapeUnicode; - } - public boolean isEscapeUnicode() { return escapeUnicode; } diff --git a/jadx-gui/src/main/java/jadx/gui/JadxGUI.java b/jadx-gui/src/main/java/jadx/gui/JadxGUI.java index 7f73bb7ac..13fd94531 100644 --- a/jadx-gui/src/main/java/jadx/gui/JadxGUI.java +++ b/jadx-gui/src/main/java/jadx/gui/JadxGUI.java @@ -26,7 +26,7 @@ public class JadxGUI { UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName()); } NLS.setLocale(settings.getLangLocale()); - SwingUtilities.invokeLater(new MainWindow(settings)::open); + SwingUtilities.invokeLater(new MainWindow(settings)::init); } catch (Exception e) { LOG.error("Error: {}", e.getMessage(), e); System.exit(1); diff --git a/jadx-gui/src/main/java/jadx/gui/JadxWrapper.java b/jadx-gui/src/main/java/jadx/gui/JadxWrapper.java index 5fa6eb6f5..746cc24ad 100644 --- a/jadx-gui/src/main/java/jadx/gui/JadxWrapper.java +++ b/jadx-gui/src/main/java/jadx/gui/JadxWrapper.java @@ -47,27 +47,24 @@ public class JadxWrapper { } public void saveAll(final File dir, final ProgressMonitor progressMonitor) { - Runnable save = new Runnable() { - @Override - public void run() { - try { - decompiler.getArgs().setRootDir(dir); - ThreadPoolExecutor ex = (ThreadPoolExecutor) decompiler.getSaveExecutor(); - ex.shutdown(); - while (ex.isTerminating()) { - long total = ex.getTaskCount(); - long done = ex.getCompletedTaskCount(); - progressMonitor.setProgress((int) (done * 100.0 / total)); - Thread.sleep(500); - } - progressMonitor.close(); - LOG.info("decompilation complete, freeing memory ..."); - decompiler.getClasses().forEach(JavaClass::unload); - LOG.info("done"); - } catch (InterruptedException e) { - LOG.error("Save interrupted", e); - Thread.currentThread().interrupt(); + Runnable save = () -> { + try { + decompiler.getArgs().setRootDir(dir); + ThreadPoolExecutor ex = (ThreadPoolExecutor) decompiler.getSaveExecutor(); + ex.shutdown(); + while (ex.isTerminating()) { + long total = ex.getTaskCount(); + long done = ex.getCompletedTaskCount(); + progressMonitor.setProgress((int) (done * 100.0 / total)); + Thread.sleep(500); } + progressMonitor.close(); + LOG.info("decompilation complete, freeing memory ..."); + decompiler.getClasses().forEach(JavaClass::unload); + LOG.info("done"); + } catch (InterruptedException e) { + LOG.error("Save interrupted", e); + Thread.currentThread().interrupt(); } }; new Thread(save).start(); diff --git a/jadx-gui/src/main/java/jadx/gui/jobs/BackgroundWorker.java b/jadx-gui/src/main/java/jadx/gui/jobs/BackgroundWorker.java index 8d666e5e1..9fb93a8c2 100644 --- a/jadx-gui/src/main/java/jadx/gui/jobs/BackgroundWorker.java +++ b/jadx-gui/src/main/java/jadx/gui/jobs/BackgroundWorker.java @@ -58,8 +58,7 @@ public class BackgroundWorker extends SwingWorker { if (searchIndex != null && searchIndex.getSkippedCount() > 0) { LOG.warn("Indexing of some classes skipped, count: {}, low memory: {}", searchIndex.getSkippedCount(), Utils.memoryInfo()); - String msg = NLS.str("message.indexingClassesSkipped"); - msg = String.format(msg, searchIndex.getSkippedCount()); + String msg = NLS.str("message.indexingClassesSkipped", searchIndex.getSkippedCount()); JOptionPane.showMessageDialog(null, msg); } } catch (Exception e) { diff --git a/jadx-gui/src/main/java/jadx/gui/settings/JadxProject.java b/jadx-gui/src/main/java/jadx/gui/settings/JadxProject.java new file mode 100644 index 000000000..c6e0aa2fe --- /dev/null +++ b/jadx-gui/src/main/java/jadx/gui/settings/JadxProject.java @@ -0,0 +1,127 @@ +package jadx.gui.settings; + +import java.io.BufferedWriter; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Arrays; +import java.util.List; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; + +import jadx.gui.utils.PathTypeAdapter; + +public class JadxProject { + + private static final Logger LOG = LoggerFactory.getLogger(JadxProject.class); + private static final int CURRENT_SETTINGS_VERSION = 0; + + public static final String PROJECT_EXTENSION = "jadx"; + + private static final Gson GSON = new GsonBuilder() + .registerTypeHierarchyAdapter(Path.class, PathTypeAdapter.singleton()) + .create(); + + private transient JadxSettings settings; + private transient String name = "New Project"; + private transient Path projectPath; + private List filesPath; + private transient boolean saved; + private transient boolean initial = true; + + private int projectVersion = 0; + + public JadxProject(JadxSettings settings) { + this.settings = settings; + } + + public Path getProjectPath() { + return projectPath; + } + + private void setProjectPath(Path projectPath) { + this.projectPath = projectPath; + if (projectVersion != CURRENT_SETTINGS_VERSION) { + upgradeSettings(projectVersion); + } + name = projectPath.getFileName().toString(); + name = name.substring(0, name.lastIndexOf('.')); + changed(); + } + + public Path getFilePath() { + return filesPath == null ? null : filesPath.get(0); + } + + public void setFilePath(Path filePath) { + if (!filePath.equals(getFilePath())) { + this.filesPath = Arrays.asList(filePath); + changed(); + } + } + + private void changed() { + if (settings.isAutoSaveProject()) { + save(); + } + else { + saved = false; + } + initial = false; + } + + public String getName() { + return name; + } + + public boolean isSaved() { + return saved; + } + + public boolean isInitial() { + return initial; + } + + public void saveAs(Path path) { + setProjectPath(path); + save(); + } + + public void save() { + try (BufferedWriter writer = Files.newBufferedWriter(getProjectPath())) { + writer.write(GSON.toJson(this)); + saved = true; + } catch (Exception e) { + LOG.error("Error saving project", e); + } + } + + public static JadxProject from(Path path, JadxSettings settings) { + try { + List lines = Files.readAllLines(path); + + if (!lines.isEmpty()) { + JadxProject project = GSON.fromJson(lines.get(0), JadxProject.class); + project.settings = settings; + project.setProjectPath(path); + project.saved = true; + return project; + } + } catch (Exception e) { + LOG.error("Error loading project", e); + } + return null; + } + + private void upgradeSettings(int fromVersion) { + LOG.debug("upgrade settings from version: {} to {}", fromVersion, CURRENT_SETTINGS_VERSION); + if (fromVersion == 0) { + fromVersion++; + } + projectVersion = CURRENT_SETTINGS_VERSION; + save(); + } +} 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 64680c03a..51d6e079b 100644 --- a/jadx-gui/src/main/java/jadx/gui/settings/JadxSettings.java +++ b/jadx-gui/src/main/java/jadx/gui/settings/JadxSettings.java @@ -4,6 +4,8 @@ import java.awt.Font; import java.awt.GraphicsDevice; import java.awt.GraphicsEnvironment; import java.awt.Window; +import java.nio.file.Path; +import java.nio.file.Paths; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; @@ -27,25 +29,27 @@ import jadx.gui.utils.Utils; public class JadxSettings extends JadxCLIArgs { private static final Logger LOG = LoggerFactory.getLogger(JadxSettings.class); - private static final String USER_HOME = System.getProperty("user.home"); - private static final int RECENT_FILES_COUNT = 15; - private static final int CURRENT_SETTINGS_VERSION = 8; + private static final Path USER_HOME = Paths.get(System.getProperty("user.home")); + private static final int RECENT_PROJECTS_COUNT = 15; + private static final int CURRENT_SETTINGS_VERSION = 9; private static final Font DEFAULT_FONT = new RSyntaxTextArea().getFont(); static final Set SKIP_FIELDS = new HashSet<>(Arrays.asList( "files", "input", "outDir", "outDirSrc", "outDirRes", "verbose", "printVersion", "printHelp" )); - private String lastOpenFilePath = USER_HOME; - private String lastSaveFilePath = USER_HOME; + private Path lastSaveProjectPath = USER_HOME; + private Path lastOpenFilePath = USER_HOME; + private Path lastSaveFilePath = USER_HOME; private boolean flattenPackage = false; private boolean checkForUpdates = false; - private List recentFiles = new ArrayList<>(); + private List recentProjects = new ArrayList<>(); private String fontStr = ""; private String editorThemePath = ""; private LangLocale langLocale = NLS.defaultLocale(); private boolean autoStartJobs = false; protected String excludedPackages = ""; + private boolean autoSaveProject = false; private boolean showHeapUsageBar = true; @@ -84,20 +88,29 @@ public class JadxSettings extends JadxCLIArgs { } } - public String getLastOpenFilePath() { + public Path getLastOpenFilePath() { return lastOpenFilePath; } - public void setLastOpenFilePath(String lastOpenFilePath) { + public void setLastOpenFilePath(Path lastOpenFilePath) { this.lastOpenFilePath = lastOpenFilePath; partialSync(settings -> settings.lastOpenFilePath = JadxSettings.this.lastOpenFilePath); } - public String getLastSaveFilePath() { + public Path getLastSaveProjectPath() { + return lastSaveProjectPath; + } + + public Path getLastSaveFilePath() { return lastSaveFilePath; } - public void setLastSaveFilePath(String lastSaveFilePath) { + public void setLastSaveProjectPath(Path lastSaveProjectPath) { + this.lastSaveProjectPath = lastSaveProjectPath; + partialSync(settings -> settings.lastSaveProjectPath = JadxSettings.this.lastSaveProjectPath); + } + + public void setLastSaveFilePath(Path lastSaveFilePath) { this.lastSaveFilePath = lastSaveFilePath; partialSync(settings -> settings.lastSaveFilePath = JadxSettings.this.lastSaveFilePath); } @@ -120,18 +133,18 @@ public class JadxSettings extends JadxCLIArgs { sync(); } - public Iterable getRecentFiles() { - return recentFiles; + public Iterable getRecentProjects() { + return recentProjects; } - public void addRecentFile(String filePath) { - recentFiles.remove(filePath); - recentFiles.add(0, filePath); - int count = recentFiles.size(); - if (count > RECENT_FILES_COUNT) { - recentFiles.subList(RECENT_FILES_COUNT, count).clear(); + public void addRecentProject(Path projectPath) { + recentProjects.remove(projectPath); + recentProjects.add(0, projectPath); + int count = recentProjects.size(); + if (count > RECENT_PROJECTS_COUNT) { + recentProjects.subList(RECENT_PROJECTS_COUNT, count).clear(); } - partialSync(settings -> settings.recentFiles = recentFiles); + partialSync(settings -> settings.recentProjects = recentProjects); } public void saveWindowPos(Window window) { @@ -265,6 +278,14 @@ public class JadxSettings extends JadxCLIArgs { this.autoStartJobs = autoStartJobs; } + public boolean isAutoSaveProject() { + return autoSaveProject; + } + + public void setAutoSaveProject(boolean autoSaveProject) { + this.autoSaveProject = autoSaveProject; + } + public void setExportAsGradleProject(boolean exportAsGradleProject) { this.exportAsGradleProject = exportAsGradleProject; } @@ -343,6 +364,10 @@ public class JadxSettings extends JadxCLIArgs { outDir = null; outDirSrc = null; outDirRes = null; + fromVersion++; + } + if (fromVersion == 8) { + fromVersion++; } settingsVersion = CURRENT_SETTINGS_VERSION; sync(); 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 30f3c44ca..2f949e744 100644 --- a/jadx-gui/src/main/java/jadx/gui/settings/JadxSettingsAdapter.java +++ b/jadx-gui/src/main/java/jadx/gui/settings/JadxSettingsAdapter.java @@ -1,17 +1,20 @@ package jadx.gui.settings; import java.lang.reflect.Modifier; +import java.nio.file.Path; import java.util.prefs.Preferences; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + import com.google.gson.ExclusionStrategy; import com.google.gson.FieldAttributes; import com.google.gson.Gson; import com.google.gson.GsonBuilder; import com.google.gson.InstanceCreator; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import jadx.gui.JadxGUI; +import jadx.gui.utils.PathTypeAdapter; public class JadxSettingsAdapter { @@ -34,7 +37,10 @@ public class JadxSettingsAdapter { return false; } }; - private static final GsonBuilder GSON_BUILDER = new GsonBuilder().setExclusionStrategies(EXCLUDE_FIELDS); + private static final GsonBuilder GSON_BUILDER = new GsonBuilder() + .setExclusionStrategies(EXCLUDE_FIELDS) + .registerTypeHierarchyAdapter(Path.class, PathTypeAdapter.singleton()) + ; private static final Gson GSON = GSON_BUILDER.create(); private JadxSettingsAdapter() { 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 0646a1b40..883237c49 100644 --- a/jadx-gui/src/main/java/jadx/gui/settings/JadxSettingsWindow.java +++ b/jadx-gui/src/main/java/jadx/gui/settings/JadxSettingsWindow.java @@ -52,6 +52,7 @@ public class JadxSettingsWindow extends JDialog { panel.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10)); panel.add(makeDeobfuscationGroup()); panel.add(makeDecompilationGroup()); + panel.add(makeProjectGroup()); panel.add(makeEditorGroup()); panel.add(makeOtherGroup()); @@ -168,6 +169,18 @@ public class JadxSettingsWindow extends JDialog { connectedComponents.forEach(comp -> comp.setEnabled(enabled)); } + private SettingsGroup makeProjectGroup() { + JCheckBox autoSave = new JCheckBox(); + autoSave.setSelected(settings.isAutoSaveProject()); + autoSave.addItemListener(e -> + settings.setAutoSaveProject(e.getStateChange() == ItemEvent.SELECTED)); + + SettingsGroup group = new SettingsGroup(NLS.str("preferences.project")); + group.addRow(NLS.str("preferences.autoSave"), autoSave); + + return group; + } + private SettingsGroup makeEditorGroup() { JButton fontBtn = new JButton(NLS.str("preferences.select_font")); @@ -186,9 +199,9 @@ public class JadxSettingsWindow extends JDialog { mainWindow.loadSettings(); }); - SettingsGroup other = new SettingsGroup(NLS.str("preferences.editor")); - JLabel fontLabel = other.addRow(getFontLabelStr(), fontBtn); - other.addRow(NLS.str("preferences.theme"), themesCbx); + SettingsGroup group = new SettingsGroup(NLS.str("preferences.editor")); + JLabel fontLabel = group.addRow(getFontLabelStr(), fontBtn); + group.addRow(NLS.str("preferences.theme"), themesCbx); fontBtn.addMouseListener(new MouseAdapter() { @Override @@ -205,7 +218,7 @@ public class JadxSettingsWindow extends JDialog { } } }); - return other; + return group; } private String getFontLabelStr() { @@ -263,7 +276,7 @@ public class JadxSettingsWindow extends JDialog { autoStartJobs.addItemListener(e -> settings.setAutoStartJobs(e.getStateChange() == ItemEvent.SELECTED)); JCheckBox escapeUnicode = new JCheckBox(); - escapeUnicode.setSelected(settings.escapeUnicode()); + escapeUnicode.setSelected(settings.isEscapeUnicode()); escapeUnicode.addItemListener(e -> { settings.setEscapeUnicode(e.getStateChange() == ItemEvent.SELECTED); needReload(); @@ -333,12 +346,12 @@ public class JadxSettingsWindow extends JDialog { needReload(); }); - SettingsGroup other = new SettingsGroup(NLS.str("preferences.other")); - other.addRow(NLS.str("preferences.language"), languageCbx); - other.addRow(NLS.str("preferences.check_for_updates"), update); - other.addRow(NLS.str("preferences.cfg"), cfg); - other.addRow(NLS.str("preferences.raw_cfg"), rawCfg); - return other; + SettingsGroup group = new SettingsGroup(NLS.str("preferences.other")); + group.addRow(NLS.str("preferences.language"), languageCbx); + group.addRow(NLS.str("preferences.check_for_updates"), update); + group.addRow(NLS.str("preferences.cfg"), cfg); + group.addRow(NLS.str("preferences.raw_cfg"), rawCfg); + return group; } private void needReload() { diff --git a/jadx-gui/src/main/java/jadx/gui/treemodel/ApkSignature.java b/jadx-gui/src/main/java/jadx/gui/treemodel/ApkSignature.java index 9e9f3f266..08c32a31e 100644 --- a/jadx-gui/src/main/java/jadx/gui/treemodel/ApkSignature.java +++ b/jadx-gui/src/main/java/jadx/gui/treemodel/ApkSignature.java @@ -77,15 +77,15 @@ public class ApkSignature extends JNode { final String err = NLS.str("apkSignature.errors"); final String warn = NLS.str("apkSignature.warnings"); - final String sigSucc = NLS.str("apkSignature.signatureSuccess"); - final String sigFail = NLS.str("apkSignature.signatureFailed"); + final String sigSuccKey = "apkSignature.signatureSuccess"; + final String sigFailKey = "apkSignature.signatureFailed"; writeIssues(builder, err, result.getErrors()); writeIssues(builder, warn, result.getWarnings()); if (!result.getV1SchemeSigners().isEmpty()) { builder.append("

"); - builder.escape(String.format(result.isVerifiedUsingV1Scheme() ? sigSucc : sigFail, 1)); + builder.escape(NLS.str(result.isVerifiedUsingV1Scheme() ? sigSuccKey : sigFailKey, 1)); builder.append("

\n"); builder.append("
"); @@ -106,7 +106,7 @@ public class ApkSignature extends JNode { } if (!result.getV2SchemeSigners().isEmpty()) { builder.append("

"); - builder.escape(String.format(result.isVerifiedUsingV2Scheme() ? sigSucc : sigFail, 2)); + builder.escape(NLS.str(result.isVerifiedUsingV2Scheme() ? sigSuccKey : sigFailKey, 2)); builder.append("

\n"); builder.append("
"); diff --git a/jadx-gui/src/main/java/jadx/gui/ui/CommonSearchDialog.java b/jadx-gui/src/main/java/jadx/gui/ui/CommonSearchDialog.java index 307d6ed80..ffbab7c92 100644 --- a/jadx-gui/src/main/java/jadx/gui/ui/CommonSearchDialog.java +++ b/jadx-gui/src/main/java/jadx/gui/ui/CommonSearchDialog.java @@ -232,11 +232,10 @@ public abstract class CommonSearchDialog extends JDialog { } protected void updateProgressLabel() { - String statusText = String.format( - NLS.str("search_dialog.info_label"), - resultsModel.getDisplayedResultsStart(), - resultsModel.getDisplayedResultsEnd(), - resultsModel.getResultCount() + String statusText = NLS.str("search_dialog.info_label", + resultsModel.getDisplayedResultsStart(), + resultsModel.getDisplayedResultsEnd(), + resultsModel.getResultCount() ); resultsInfoLabel.setText(statusText); } diff --git a/jadx-gui/src/main/java/jadx/gui/ui/HeapUsageBar.java b/jadx-gui/src/main/java/jadx/gui/ui/HeapUsageBar.java index 5155437b1..d6add3d53 100644 --- a/jadx-gui/src/main/java/jadx/gui/ui/HeapUsageBar.java +++ b/jadx-gui/src/main/java/jadx/gui/ui/HeapUsageBar.java @@ -25,11 +25,9 @@ public class HeapUsageBar extends JProgressBar implements ActionListener { private final transient Runtime runtime = Runtime.getRuntime(); private final transient Timer timer; - private final String textFormat; private final double maxGB; public HeapUsageBar() { - this.textFormat = NLS.str("heapUsage.text"); setBorderPainted(false); setStringPainted(true); setValue(10); @@ -54,7 +52,7 @@ public class HeapUsageBar extends JProgressBar implements ActionListener { long used = runtime.totalMemory() - runtime.freeMemory(); int usedKB = (int) (used / 1024); setValue(usedKB); - setString(String.format(textFormat, (usedKB / TWO_TO_20), maxGB)); + setString(NLS.str("heapUsage.text", (usedKB / TWO_TO_20), maxGB)); if ((used + Utils.MIN_FREE_MEMORY) > runtime.maxMemory()) { setForeground(RED); 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 c635cbfd4..3f03c852d 100644 --- a/jadx-gui/src/main/java/jadx/gui/ui/MainDropTarget.java +++ b/jadx-gui/src/main/java/jadx/gui/ui/MainDropTarget.java @@ -62,7 +62,7 @@ public class MainDropTarget implements DropTargetListener { if (!transferData.isEmpty()) { dtde.dropComplete(true); // load first file - mainWindow.openFile(transferData.get(0)); + mainWindow.open(transferData.get(0).toPath()); } } catch (Exception e) { LOG.error("File drop operation failed", e); 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 ea8ce0552..c5dfb62c6 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,54 @@ package jadx.gui.ui; -import javax.swing.*; +import static javax.swing.KeyStroke.getKeyStroke; + +import java.awt.BorderLayout; +import java.awt.Component; +import java.awt.DisplayMode; +import java.awt.Font; +import java.awt.GraphicsDevice; +import java.awt.GraphicsEnvironment; +import java.awt.dnd.DnDConstants; +import java.awt.dnd.DropTarget; +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.awt.event.WindowAdapter; +import java.awt.event.WindowEvent; +import java.io.File; +import java.io.FileInputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Arrays; +import java.util.Locale; +import java.util.Timer; +import java.util.TimerTask; + +import javax.swing.AbstractAction; +import javax.swing.Action; +import javax.swing.Box; +import javax.swing.ImageIcon; +import javax.swing.JCheckBoxMenuItem; +import javax.swing.JFileChooser; +import javax.swing.JFrame; +import javax.swing.JMenu; +import javax.swing.JMenuBar; +import javax.swing.JMenuItem; +import javax.swing.JOptionPane; +import javax.swing.JPanel; +import javax.swing.JPopupMenu; +import javax.swing.JScrollPane; +import javax.swing.JSplitPane; +import javax.swing.JToggleButton; +import javax.swing.JToolBar; +import javax.swing.JTree; +import javax.swing.ProgressMonitor; +import javax.swing.SwingUtilities; +import javax.swing.WindowConstants; import javax.swing.event.MenuEvent; import javax.swing.event.MenuListener; import javax.swing.event.TreeExpansionEvent; @@ -12,20 +60,6 @@ import javax.swing.tree.DefaultTreeModel; import javax.swing.tree.TreeNode; import javax.swing.tree.TreePath; import javax.swing.tree.TreeSelectionModel; -import java.awt.*; -import java.awt.dnd.DnDConstants; -import java.awt.dnd.DropTarget; -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.io.File; -import java.io.FileInputStream; -import java.util.Arrays; -import java.util.Timer; -import java.util.TimerTask; import org.fife.ui.rsyntaxtextarea.Theme; import org.slf4j.Logger; @@ -37,6 +71,7 @@ import jadx.gui.JadxWrapper; import jadx.gui.jobs.BackgroundWorker; import jadx.gui.jobs.DecompileJob; import jadx.gui.jobs.IndexJob; +import jadx.gui.settings.JadxProject; import jadx.gui.settings.JadxSettings; import jadx.gui.settings.JadxSettingsWindow; import jadx.gui.treemodel.ApkSignature; @@ -56,8 +91,6 @@ import jadx.gui.utils.Link; import jadx.gui.utils.NLS; import jadx.gui.utils.Utils; -import static javax.swing.KeyStroke.getKeyStroke; - @SuppressWarnings("serial") public class MainWindow extends JFrame { private static final Logger LOG = LoggerFactory.getLogger(MainWindow.class); @@ -86,6 +119,9 @@ public class MainWindow extends JFrame { private final transient JadxWrapper wrapper; private final transient JadxSettings settings; private final transient CacheObject cacheObject; + private transient JadxProject project; + private transient Action newProjectAction; + private transient Action saveProjectAction; private JPanel mainPanel; @@ -119,20 +155,26 @@ public class MainWindow extends JFrame { Utils.setWindowIcons(this); loadSettings(); checkForUpdate(); + newProject(); } - public void open() { + public void init() { pack(); setLocationAndPosition(); heapUsageBar.setVisible(settings.isShowHeapUsageBar()); setVisible(true); setLocationRelativeTo(null); - setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE); + setDefaultCloseOperation(WindowConstants.DO_NOTHING_ON_CLOSE); + addWindowListener(new WindowAdapter() { + public void windowClosing(WindowEvent e) { + closeWindow(); + } + }); if (settings.getFiles().isEmpty()) { - openFile(); + openFileOrProject(); } else { - openFile(new File(settings.getFiles().get(0))); + open(Paths.get(settings.getFiles().get(0))); } } @@ -146,7 +188,7 @@ public class MainWindow extends JFrame { SwingUtilities.invokeLater(new Runnable() { @Override public void run() { - updateLink.setText(String.format(NLS.str("menu.update_label"), r.getName())); + updateLink.setText(NLS.str("menu.update_label", r.getName())); updateLink.setVisible(true); } }); @@ -154,33 +196,160 @@ public class MainWindow extends JFrame { }); } - public void openFile() { + public void openFileOrProject() { JFileChooser fileChooser = new JFileChooser(); fileChooser.setAcceptAllFileFilterUsed(true); - String[] exts = {"apk", "dex", "jar", "class", "zip", "aar", "arsc"}; + String[] exts = {JadxProject.PROJECT_EXTENSION, "apk", "dex", "jar", "class", "zip", "aar", "arsc"}; String description = "supported files: " + Arrays.toString(exts).replace('[', '(').replace(']', ')'); fileChooser.setFileFilter(new FileNameExtensionFilter(description, exts)); fileChooser.setToolTipText(NLS.str("file.open_action")); - String currentDirectory = settings.getLastOpenFilePath(); - if (!currentDirectory.isEmpty()) { - fileChooser.setCurrentDirectory(new File(currentDirectory)); + Path currentDirectory = settings.getLastOpenFilePath(); + if (currentDirectory != null) { + fileChooser.setCurrentDirectory(currentDirectory.toFile()); } int ret = fileChooser.showDialog(mainPanel, NLS.str("file.open_title")); if (ret == JFileChooser.APPROVE_OPTION) { - settings.setLastOpenFilePath(fileChooser.getCurrentDirectory().getPath()); - openFile(fileChooser.getSelectedFile()); + settings.setLastOpenFilePath(fileChooser.getCurrentDirectory().toPath()); + open(fileChooser.getSelectedFile().toPath()); } } - public void openFile(File file) { + private void newProject() { + if (!ensureProjectIsSaved()) { + return; + } + project = new JadxProject(settings); + update(); + clearTree(); + } + + private void clearTree() { tabbedPane.closeAllTabs(); resetCache(); - wrapper.openFile(file); - deobfToggleBtn.setSelected(settings.isDeobfuscationOn()); - settings.addRecentFile(file.getAbsolutePath()); - initTree(); - setTitle(DEFAULT_TITLE + " - " + file.getName()); - runBackgroundJobs(); + treeRoot = null; + treeModel.setRoot(treeRoot); + treeModel.reload(); + } + + private void saveProject() { + if (project.getProjectPath() == null) { + saveProjectAs(); + } + else { + project.save(); + update(); + } + } + + private void saveProjectAs() { + JFileChooser fileChooser = new JFileChooser(); + fileChooser.setAcceptAllFileFilterUsed(true); + String[] exts = {JadxProject.PROJECT_EXTENSION}; + String description = "supported files: " + Arrays.toString(exts).replace('[', '(').replace(']', ')'); + fileChooser.setFileFilter(new FileNameExtensionFilter(description, exts)); + fileChooser.setToolTipText(NLS.str("file.save_project")); + Path currentDirectory = settings.getLastSaveProjectPath(); + if (currentDirectory != null) { + fileChooser.setCurrentDirectory(currentDirectory.toFile()); + } + int ret = fileChooser.showSaveDialog(mainPanel); + if (ret == JFileChooser.APPROVE_OPTION) { + settings.setLastSaveProjectPath(fileChooser.getCurrentDirectory().toPath()); + + Path path = fileChooser.getSelectedFile().toPath(); + if (!path.getFileName().toString().toLowerCase(Locale.ROOT).endsWith(JadxProject.PROJECT_EXTENSION)) { + path = path.resolveSibling(path.getFileName() + "." + JadxProject.PROJECT_EXTENSION); + } + + if (Files.exists(path)) { + int res = JOptionPane.showConfirmDialog( + this, + NLS.str("confirm.save_as_message", path.getFileName()), + NLS.str("confirm.save_as_title"), + JOptionPane.YES_NO_OPTION); + if (res == JOptionPane.NO_OPTION) { + return; + } + } + project.saveAs(path); + update(); + } + } + + void open(Path path) { + if (path.getFileName().toString().toLowerCase(Locale.ROOT) + .endsWith(JadxProject.PROJECT_EXTENSION)) { + openProject(path); + } + else { + project.setFilePath(path); + tabbedPane.closeAllTabs(); + resetCache(); + wrapper.openFile(path.toFile()); + deobfToggleBtn.setSelected(settings.isDeobfuscationOn()); + initTree(); + update(); + runBackgroundJobs(); + } + } + + private boolean ensureProjectIsSaved() { + if (project != null && !project.isSaved() && !project.isInitial()) { + int res = JOptionPane.showConfirmDialog( + this, + NLS.str("confirm.not_saved_message"), + NLS.str("confirm.not_saved_title"), + JOptionPane.YES_NO_CANCEL_OPTION); + if (res == JOptionPane.CANCEL_OPTION) { + return false; + } + if (res == JOptionPane.YES_OPTION) { + project.save(); + } + } + return true; + } + + private void openProject(Path path) { + if (!ensureProjectIsSaved()) { + return; + } + project = JadxProject.from(path, settings); + if (project == null) { + JOptionPane.showMessageDialog( + this, + NLS.str("msg.project_error"), + NLS.str("msg.project_error_title"), + JOptionPane.INFORMATION_MESSAGE + ); + return; + } + update(); + settings.addRecentProject(path); + Path filePath = project.getFilePath(); + if (filePath == null) { + clearTree(); + } + else { + open(filePath); + } + } + + private void update() { + newProjectAction.setEnabled(!project.isInitial()); + saveProjectAction.setEnabled(!project.isSaved()); + + Path projectPath = project.getProjectPath(); + String pathString; + if (projectPath == null) { + pathString = ""; + } + else { + pathString = " [" + projectPath.getParent().toAbsolutePath() + ']'; + } + setTitle((project.isSaved() ? "" : '*') + + project.getName() + pathString + " - " + DEFAULT_TITLE); + } protected void resetCache() { @@ -215,7 +384,7 @@ public class MainWindow extends JFrame { public void reOpenFile() { File openedFile = wrapper.getOpenFile(); if (openedFile != null) { - openFile(openedFile); + open(openedFile.toPath()); } } @@ -224,12 +393,12 @@ public class MainWindow extends JFrame { fileChooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY); fileChooser.setToolTipText(NLS.str("file.save_all_msg")); - String currentDirectory = settings.getLastSaveFilePath(); - if (!currentDirectory.isEmpty()) { - fileChooser.setCurrentDirectory(new File(currentDirectory)); + Path currentDirectory = settings.getLastSaveFilePath(); + if (currentDirectory != null) { + fileChooser.setCurrentDirectory(currentDirectory.toFile()); } - int ret = fileChooser.showDialog(mainPanel, NLS.str("file.select")); + int ret = fileChooser.showSaveDialog(mainPanel); if (ret == JFileChooser.APPROVE_OPTION) { JadxArgs decompilerArgs = wrapper.getArgs(); decompilerArgs.setExportAsGradleProject(export); @@ -240,7 +409,7 @@ public class MainWindow extends JFrame { decompilerArgs.setSkipSources(settings.isSkipSources()); decompilerArgs.setSkipResources(settings.isSkipResources()); } - settings.setLastSaveFilePath(fileChooser.getCurrentDirectory().getPath()); + settings.setLastSaveFilePath(fileChooser.getCurrentDirectory().toPath()); ProgressMonitor progressMonitor = new ProgressMonitor(mainPanel, NLS.str("msg.saving_sources"), "", 0, 100); progressMonitor.setMillisToPopup(0); wrapper.saveAll(fileChooser.getSelectedFile(), progressMonitor); @@ -351,12 +520,36 @@ public class MainWindow extends JFrame { Action openAction = new AbstractAction(NLS.str("file.open_action"), ICON_OPEN) { @Override public void actionPerformed(ActionEvent e) { - openFile(); + openFileOrProject(); } }; openAction.putValue(Action.SHORT_DESCRIPTION, NLS.str("file.open_action")); openAction.putValue(Action.ACCELERATOR_KEY, getKeyStroke(KeyEvent.VK_O, KeyEvent.CTRL_DOWN_MASK)); + newProjectAction = new AbstractAction(NLS.str("file.new_project")) { + @Override + public void actionPerformed(ActionEvent e) { + newProject(); + } + }; + newProjectAction.putValue(Action.SHORT_DESCRIPTION, NLS.str("file.new_project")); + + saveProjectAction = new AbstractAction(NLS.str("file.save_project")) { + @Override + public void actionPerformed(ActionEvent e) { + saveProject(); + } + }; + saveProjectAction.putValue(Action.SHORT_DESCRIPTION, NLS.str("file.save_project")); + + Action saveProjectAsAction = new AbstractAction(NLS.str("file.save_project_as")) { + @Override + public void actionPerformed(ActionEvent e) { + saveProjectAs(); + } + }; + saveProjectAsAction.putValue(Action.SHORT_DESCRIPTION, NLS.str("file.save_project_as")); + Action saveAllAction = new AbstractAction(NLS.str("file.save_all"), ICON_SAVE_ALL) { @Override public void actionPerformed(ActionEvent e) { @@ -375,8 +568,8 @@ public class MainWindow extends JFrame { exportAction.putValue(Action.SHORT_DESCRIPTION, NLS.str("file.export_gradle")); exportAction.putValue(Action.ACCELERATOR_KEY, getKeyStroke(KeyEvent.VK_E, KeyEvent.CTRL_DOWN_MASK)); - JMenu recentFiles = new JMenu(NLS.str("menu.recent_files")); - recentFiles.addMenuListener(new RecentFilesMenuListener(recentFiles)); + JMenu recentProjects = new JMenu(NLS.str("menu.recent_projects")); + recentProjects.addMenuListener(new RecentProjectsMenuListener(recentProjects)); Action prefsAction = new AbstractAction(NLS.str("menu.preferences"), ICON_PREF) { @Override @@ -491,10 +684,15 @@ public class MainWindow extends JFrame { JMenu file = new JMenu(NLS.str("menu.file")); file.setMnemonic(KeyEvent.VK_F); file.add(openAction); + file.addSeparator(); + file.add(newProjectAction); + file.add(saveProjectAction); + file.add(saveProjectAsAction); + file.addSeparator(); file.add(saveAllAction); file.add(exportAction); file.addSeparator(); - file.add(recentFiles); + file.add(recentProjects); file.addSeparator(); file.add(prefsAction); file.addSeparator(); @@ -694,11 +892,13 @@ public class MainWindow extends JFrame { tabbedPane.loadSettings(); } - @Override - public void dispose() { + private void closeWindow() { + if (!ensureProjectIsSaved()) { + return; + } settings.saveWindowPos(this); cancelBackgroundJobs(); - super.dispose(); + dispose(); } public JadxWrapper getWrapper() { @@ -721,28 +921,27 @@ public class MainWindow extends JFrame { return backgroundWorker; } - private class RecentFilesMenuListener implements MenuListener { - private final JMenu recentFiles; + private class RecentProjectsMenuListener implements MenuListener { + private final JMenu recentProjects; - public RecentFilesMenuListener(JMenu recentFiles) { - this.recentFiles = recentFiles; + public RecentProjectsMenuListener(JMenu recentProjects) { + this.recentProjects = recentProjects; } @Override public void menuSelected(MenuEvent menuEvent) { - recentFiles.removeAll(); + recentProjects.removeAll(); File openFile = wrapper.getOpenFile(); - String currentFile = openFile == null ? "" : openFile.getAbsolutePath(); - for (final String file : settings.getRecentFiles()) { - if (file.equals(currentFile)) { - continue; + Path currentPath = openFile == null ? null : openFile.toPath(); + for (final Path path : settings.getRecentProjects()) { + if (!path.equals(currentPath)) { + JMenuItem menuItem = new JMenuItem(path.toAbsolutePath().toString()); + recentProjects.add(menuItem); + menuItem.addActionListener(e -> open(path)); } - JMenuItem menuItem = new JMenuItem(file); - recentFiles.add(menuItem); - menuItem.addActionListener(e -> openFile(new File(file))); } - if (recentFiles.getItemCount() == 0) { - recentFiles.add(new JMenuItem(NLS.str("menu.no_recent_files"))); + if (recentProjects.getItemCount() == 0) { + recentProjects.add(new JMenuItem(NLS.str("menu.no_recent_projects"))); } } diff --git a/jadx-gui/src/main/java/jadx/gui/utils/NLS.java b/jadx-gui/src/main/java/jadx/gui/utils/NLS.java index a835c3d8a..2c1a295ef 100644 --- a/jadx-gui/src/main/java/jadx/gui/utils/NLS.java +++ b/jadx-gui/src/main/java/jadx/gui/utils/NLS.java @@ -61,12 +61,14 @@ public class NLS { i18nMessagesMap.put(locale, bundle); } - public static String str(String key) { + public static String str(String key, Object... parameters) { + String value; try { - return localizedMessagesMap.getString(key); + value = localizedMessagesMap.getString(key); } catch (MissingResourceException e) { - return fallbackMessagesMap.getString(key); // definitely exists + value = fallbackMessagesMap.getString(key); // definitely exists } + return String.format(value, parameters); } public static String str(String key, LangLocale locale) { diff --git a/jadx-gui/src/main/java/jadx/gui/utils/PathTypeAdapter.java b/jadx-gui/src/main/java/jadx/gui/utils/PathTypeAdapter.java new file mode 100644 index 000000000..d36b67e3e --- /dev/null +++ b/jadx-gui/src/main/java/jadx/gui/utils/PathTypeAdapter.java @@ -0,0 +1,44 @@ +package jadx.gui.utils; + +import java.io.IOException; +import java.nio.file.Path; +import java.nio.file.Paths; + +import com.google.gson.TypeAdapter; +import com.google.gson.stream.JsonReader; +import com.google.gson.stream.JsonToken; +import com.google.gson.stream.JsonWriter; + +public class PathTypeAdapter { + + private static TypeAdapter SINGLETON; + + public static TypeAdapter singleton() { + if (SINGLETON == null) { + SINGLETON = new TypeAdapter() { + + @Override + public void write(JsonWriter out, Path value) throws IOException { + if (value == null) { + out.nullValue(); + } else { + out.value(value.toAbsolutePath().toString()); + } + } + + @Override + public Path read(JsonReader in) throws IOException { + if (in.peek() == JsonToken.NULL) { + in.nextNull(); + return null; + } + return Paths.get(in.nextString()); + } + }; + } + return SINGLETON; + } + + private PathTypeAdapter() { + } +} 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 ed42bbad6..e4f040a78 100644 --- a/jadx-gui/src/main/resources/i18n/Messages_en_US.properties +++ b/jadx-gui/src/main/resources/i18n/Messages_en_US.properties @@ -2,8 +2,8 @@ language.name=English menu.file=File menu.view=View -menu.recent_files=Recent Files -menu.no_recent_files=No recent files +menu.recent_projects=Recent projects +menu.no_recent_projects=No recent projects menu.preferences=Preferences menu.sync=Sync with editor menu.flatten=Show flatten packages @@ -20,17 +20,18 @@ menu.update_label=New version %s available! file.open_action=Open file... file.open_title=Open file +file.new_project=New project +file.save_project=Save project +file.save_project_as=Save project as... file.save_all=Save all file.export_gradle=Save as gradle project file.save_all_msg=Select directory for save decompiled sources -file.select=Select file.exit=Exit tree.sources_title=Source code tree.resources_title=Resources tree.loading=Loading... -search=Search search.previous=Previous search.next=Next search.mark_all=Mark All @@ -79,6 +80,7 @@ preferences.title=Preferences preferences.deobfuscation=Deobfuscation preferences.editor=Editor preferences.decompile=Decompilation +preferences.project=Project preferences.other=Other preferences.language=Language preferences.check_for_updates=Check for updates on startup @@ -89,6 +91,7 @@ preferences.replaceConsts=Replace constants preferences.respectBytecodeAccessModifiers=Respect bytecode access modifiers preferences.useImports=Use import statements preferences.skipResourcesDecode=Don't decode resources +preferences.autoSave=Auto save preferences.threads=Processing threads count preferences.excludedPackages=Excluded packages preferences.excludedPackages.tooltip=List of space separated package names that will not be decompiled or indexed (saves RAM) @@ -116,6 +119,8 @@ msg.saving_sources=Saving sources... msg.language_changed_title=Language changed msg.language_changed=New language will be displayed the next time application starts. msg.index_not_initialized=Index not initialized, search will be disabled! +msg.project_error_title=Error +msg.project_error=Project could not be loaded popup.undo=Undo popup.redo=Redo @@ -127,11 +132,15 @@ popup.select_all=Select All popup.find_usage=Find Usage popup.exclude=Exclude +confirm.save_as_title=Confirm Save as +confirm.save_as_message=%s already exists.\nDo you want to replace it? +confirm.not_saved_title=Save project +confirm.not_saved_message=Save the current project before opening the new one? + certificate.title=Certificate certificate.cert_type=Type certificate.serialSigVer=Version certificate.serialNumber=Serial number -certificate.cert_issuer=Issuer certificate.cert_subject=Subject certificate.serialValidFrom=Valid from certificate.serialValidUntil=Valid until diff --git a/jadx-gui/src/main/resources/i18n/Messages_es_ES.properties b/jadx-gui/src/main/resources/i18n/Messages_es_ES.properties index a63d45be5..8d13d8ad1 100644 --- a/jadx-gui/src/main/resources/i18n/Messages_es_ES.properties +++ b/jadx-gui/src/main/resources/i18n/Messages_es_ES.properties @@ -2,8 +2,8 @@ language.name=Español menu.file=Archivo menu.view=Vista -menu.recent_files=Archivos recientes -menu.no_recent_files=No hay archivos recientes +#menu.recent_projects= +#menu.no_recent_projects= menu.preferences=Preferencias menu.sync=Sincronizar con el editor menu.flatten=Mostrar paquetes en vista plana @@ -20,17 +20,18 @@ menu.update_label=¡Nueva versión %s disponible! file.open_action=Abrir archivo... file.open_title=Abrir archivo +#file.new_project= +#file.save_project= +#file.save_project_as= file.save_all=Guardar todo file.export_gradle=Guardar como proyecto Gradle file.save_all_msg=Seleccionar carpeta para guardar fuentes descompiladas -file.select=Seleccionar file.exit=Salir tree.sources_title=Código fuente tree.resources_title=Recursos tree.loading=Cargando... -search=Buscar search.previous=Anterior search.next=Siguiente search.mark_all=Marcar todo @@ -79,6 +80,7 @@ preferences.title=Preferencias preferences.deobfuscation=Desofuscación preferences.editor=Editor preferences.decompile=Descompilación +#preferences.project= preferences.other=Otros preferences.language=Idioma preferences.check_for_updates=Buscar actualizaciones al iniciar @@ -89,6 +91,7 @@ preferences.replaceConsts=Reemplazar constantes #preferences.respectBytecodeAccessModifiers= #preferences.useImports= preferences.skipResourcesDecode=No descodificar recursos +#preferences.autoSave= preferences.threads=Número de hilos a procesar #preferences.excludedPackages= #preferences.excludedPackages.tooltip= @@ -116,6 +119,8 @@ msg.saving_sources=Guardando fuente... msg.language_changed_title=Idioma cambiado msg.language_changed=El nuevo idioma se mostrará la próxima vez que la aplicación se inicie. msg.index_not_initialized=Índice no inicializado, ¡la bósqueda se desactivará! +#msg.project_error_title= +#msg.project_error= popup.undo=Deshacer popup.redo=Rehacer @@ -127,11 +132,15 @@ popup.select_all=Seleccionar todo #popup.find_usage= #popup.exclude= +#confirm.save_as_title= +#confirm.save_as_message= +#confirm.not_saved_title= +#confirm.not_saved_message= + certificate.title=Certificado certificate.cert_type=Tipo certificate.serialSigVer=Versión certificate.serialNumber=Número de serial -certificate.cert_issuer=Issuer certificate.cert_subject=Subject certificate.serialValidFrom=Válido desde certificate.serialValidUntil=Válido hasta diff --git a/jadx-gui/src/main/resources/i18n/Messages_zh_CN.properties b/jadx-gui/src/main/resources/i18n/Messages_zh_CN.properties index 296041210..20982886d 100644 --- a/jadx-gui/src/main/resources/i18n/Messages_zh_CN.properties +++ b/jadx-gui/src/main/resources/i18n/Messages_zh_CN.properties @@ -2,8 +2,8 @@ language.name=中文(简体) menu.file=文件 menu.view=视图 -menu.recent_files=最近打开的文件 -menu.no_recent_files=无最近打开的文件 +#menu.recent_projects= +#menu.no_recent_projects= menu.preferences=首选项 menu.sync=与编辑器同步 menu.flatten=展开显示代码包 @@ -20,17 +20,18 @@ menu.update_label=发现新版本 %s! file.open_action=打开文件... file.open_title=打开文件 +#file.new_project= +#file.save_project= +#file.save_project_as= file.save_all=全部保存 file.export_gradle=另存为 Gradle 项目 file.save_all_msg=选择反编译资源路径 -file.select=选择 file.exit=退出 tree.sources_title=源代码 tree.resources_title=资源文件 tree.loading=稍等... -search=搜索 search.previous=上一个 search.next=下一个 search.mark_all=标记全部 @@ -79,6 +80,7 @@ preferences.title=首选项 preferences.deobfuscation=反混淆 preferences.editor=编辑器 preferences.decompile=反编译 +#preferences.project= preferences.other=其他 preferences.language=语言 preferences.check_for_updates=启动时检查更新 @@ -89,6 +91,7 @@ preferences.replaceConsts=替换常量 preferences.respectBytecodeAccessModifiers=遵守字节码访问修饰符 preferences.useImports=使用 import 语句 preferences.skipResourcesDecode=不反编译资源文件 +#preferences.autoSave= preferences.threads=并行线程数 preferences.excludedPackages=排除的包 preferences.excludedPackages.tooltip=将不被解压缩或索引的以空格分隔的包名称列表(节省 RAM) @@ -116,6 +119,8 @@ msg.saving_sources=正在导出源代码... msg.language_changed_title=语言已更改 msg.language_changed=在下次启动时将会显示新的语言。 msg.index_not_initialized=索引尚未初始化,无法进行搜索! +#msg.project_error_title= +#msg.project_error= popup.undo=撤销 popup.redo=重做 @@ -127,11 +132,15 @@ popup.select_all=全选 popup.find_usage=查找用例 #popup.exclude= +#confirm.save_as_title= +#confirm.save_as_message= +#confirm.not_saved_title= +#confirm.not_saved_message= + certificate.title=证书 certificate.cert_type=类型 certificate.serialSigVer=版本 certificate.serialNumber=序列号 -certificate.cert_issuer=颁发者 certificate.cert_subject=主题 certificate.serialValidFrom=有效期始 certificate.serialValidUntil=有效期至 diff --git a/jadx-gui/src/test/java/jadx/gui/TestI18n.java b/jadx-gui/src/test/java/jadx/gui/TestI18n.java index f3971427d..d8596ae0a 100644 --- a/jadx-gui/src/test/java/jadx/gui/TestI18n.java +++ b/jadx-gui/src/test/java/jadx/gui/TestI18n.java @@ -1,25 +1,44 @@ package jadx.gui; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.empty; import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; + import java.io.IOException; +import java.io.Reader; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; +import java.util.HashSet; +import java.util.Iterator; import java.util.List; +import java.util.Properties; +import java.util.Set; import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; public class TestI18n { + private static Path guiJavaPath; + private static Path i18nPath; + private List reference; private String referenceName; + @BeforeAll + public static void init() { + i18nPath = Paths.get("src/main/resources/i18n"); + assertTrue(Files.exists(i18nPath)); + guiJavaPath = Paths.get("src/main/java"); + assertTrue(Files.exists(guiJavaPath)); + } + @Test public void filesExactlyMatch() throws IOException { - Path path = Paths.get("./src/main/resources/i18n"); - assertTrue(Files.exists(path)); - Files.list(path).forEach(p -> { + Files.list(i18nPath).forEach(p -> { List lines; try { lines = Files.readAllLines(p); @@ -45,12 +64,12 @@ public class TestI18n { if (p0 != -1) { String prefix = line.substring(0, p0 + 1); if (i >= lines.size() || !trimComment(lines.get(i)).startsWith(prefix)) { - fail(path, i + 1); + failLine(path, i + 1); } } } if (lines.size() != reference.size()) { - fail(path, reference.size()); + failLine(path, reference.size()); } } @@ -58,7 +77,37 @@ public class TestI18n { return string.startsWith("#") ? string.substring(1) : string; } - private void fail(Path path, int line) { - Assertions.fail("I18n files " + path.getFileName() + " and " + referenceName + " differ in line " + line); + private void failLine(Path path, int line) { + fail("I18n files " + path.getFileName() + " and " + referenceName + " differ in line " + line); + } + + @Test + public void keyIsUsed() throws IOException { + Properties properties = new Properties(); + try (Reader reader = Files.newBufferedReader(i18nPath.resolve("Messages_en_US.properties"))) { + properties.load(reader); + } + + Set keys = new HashSet<>(); + for (Object key : properties.keySet()) { + keys.add("\"" + key + '"'); + } + + Files.walk(guiJavaPath).filter(p -> Files.isRegularFile(p)).forEach(p -> { + try { + List lines = Files.readAllLines(p); + for (String line : lines) { + for (Iterator it = keys.iterator(); it.hasNext(); ) { + if (line.contains(it.next())) { + it.remove(); + } + } + } + } catch (IOException e) { + throw new RuntimeException(e); + } + }); + + assertThat("keys not used", keys, empty()); } }