feat: add gradle export templates, support android app/lib and simple java

This commit is contained in:
Skylot
2025-04-29 21:07:52 +01:00
parent 9981949a2b
commit e3aa49aaa9
54 changed files with 1153 additions and 570 deletions
@@ -7,6 +7,7 @@ import javax.swing.JOptionPane;
import jadx.api.ICodeCache;
import jadx.api.utils.tasks.ITaskExecutor;
import jadx.gui.JadxWrapper;
import jadx.gui.cache.code.CodeCacheMode;
import jadx.gui.cache.code.FixedCodeCache;
import jadx.gui.ui.MainWindow;
import jadx.gui.utils.NLS;
@@ -42,9 +43,11 @@ public class ExportTask extends CancelableBackgroundTask {
private void wrapCodeCache() {
uiCodeCache = wrapper.getArgs().getCodeCache();
// do not save newly decompiled code in cache to not increase memory usage
// TODO: maybe make memory limited cache?
wrapper.getArgs().setCodeCache(new FixedCodeCache(uiCodeCache));
if (mainWindow.getSettings().getCodeCacheMode() != CodeCacheMode.DISK) {
// do not save newly decompiled code in cache to not increase memory usage
// TODO: maybe make memory limited cache?
wrapper.getArgs().setCodeCache(new FixedCodeCache(uiCodeCache));
}
}
@Override
@@ -32,7 +32,7 @@ import jadx.gui.utils.UiUtils;
public class TreeExpansionService {
private static final Logger LOG = LoggerFactory.getLogger(TreeExpansionService.class);
private static final boolean DEBUG = UiUtils.JADX_GUI_DEBUG;
private static final boolean DEBUG = false;
private static final Comparator<TreePath> PATH_LENGTH_REVERSE = Comparator.comparingInt(p -> -p.getPathCount());
@@ -74,6 +74,9 @@ import org.jetbrains.annotations.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.formdev.flatlaf.extras.FlatInspector;
import com.formdev.flatlaf.extras.FlatUIDefaultsInspector;
import ch.qos.logback.classic.Level;
import jadx.api.JadxArgs;
@@ -111,7 +114,6 @@ import jadx.gui.plugins.context.CommonGuiPluginsContext;
import jadx.gui.plugins.context.TreePopupMenuEntry;
import jadx.gui.plugins.mappings.RenameMappingsGui;
import jadx.gui.plugins.quark.QuarkDialog;
import jadx.gui.settings.ExportProjectProperties;
import jadx.gui.settings.JadxProject;
import jadx.gui.settings.JadxSettings;
import jadx.gui.settings.ui.JadxSettingsWindow;
@@ -132,9 +134,9 @@ import jadx.gui.ui.codearea.EditorViewState;
import jadx.gui.ui.dialog.ADBDialog;
import jadx.gui.ui.dialog.AboutDialog;
import jadx.gui.ui.dialog.ExceptionDialog;
import jadx.gui.ui.dialog.ExportProjectDialog;
import jadx.gui.ui.dialog.LogViewerDialog;
import jadx.gui.ui.dialog.SearchDialog;
import jadx.gui.ui.export.ExportProjectDialog;
import jadx.gui.ui.filedialog.FileDialogWrapper;
import jadx.gui.ui.filedialog.FileOpenMode;
import jadx.gui.ui.menu.HiddenMenuItem;
@@ -798,25 +800,23 @@ public class MainWindow extends JFrame {
backgroundExecutor.cancelAll();
}
private void exportProject() {
ExportProjectDialog dialog = new ExportProjectDialog(this, this::saveAll);
public void exportProject() {
ExportProjectDialog dialog = new ExportProjectDialog(this, props -> {
JadxArgs args = wrapper.getArgs();
if (props.isAsGradleMode()) {
args.setExportGradleType(props.getExportGradleType());
args.setSkipSources(false);
args.setSkipResources(false);
} else {
args.setExportGradleType(null);
args.setSkipSources(props.isSkipSources());
args.setSkipResources(props.isSkipResources());
}
backgroundExecutor.execute(new ExportTask(this, wrapper, new File(props.getExportPath())));
});
dialog.setVisible(true);
}
private void saveAll(ExportProjectProperties exportProjectProperties) {
JadxArgs decompilerArgs = wrapper.getArgs();
decompilerArgs.setExportAsGradleProject(exportProjectProperties.isAsGradleMode());
if (exportProjectProperties.isAsGradleMode()) {
decompilerArgs.setSkipSources(false);
decompilerArgs.setSkipResources(false);
} else {
decompilerArgs.setSkipSources(exportProjectProperties.isSkipSources());
decompilerArgs.setSkipResources(exportProjectProperties.isSkipResources());
}
File saveDir = new File(exportProjectProperties.getExportPath());
backgroundExecutor.execute(new ExportTask(this, wrapper, saveDir));
}
public void initTree() {
treeRoot = new JRoot(wrapper);
treeRoot.setFlatPackages(isFlattenPackage);
@@ -1422,6 +1422,11 @@ public class MainWindow extends JFrame {
mainPanel.add(bottomSplitPane, BorderLayout.CENTER);
setContentPane(mainPanel);
setTitle(DEFAULT_TITLE);
if (UiUtils.JADX_GUI_DEBUG) {
FlatInspector.install("ctrl shift alt X");
FlatUIDefaultsInspector.install("ctrl shift alt Y");
}
}
public void setLocationAndPosition() {
@@ -27,7 +27,7 @@ public abstract class CommonDialog extends JDialog {
UiUtils.addEscapeShortCutToDispose(this);
setLocationRelativeTo(null);
pack();
UiUtils.uiRunAndWait(this::pack);
Dimension minSize = getSize();
setMinimumSize(minSize);
if (!mainWindow.getSettings().loadWindowPos(this)) {
@@ -1,10 +1,9 @@
package jadx.gui.ui.dialog;
package jadx.gui.ui.export;
import java.awt.BorderLayout;
import java.awt.Container;
import java.awt.Dimension;
import java.awt.event.ItemEvent;
import java.io.File;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import java.util.function.Consumer;
@@ -12,16 +11,23 @@ import java.util.function.Consumer;
import javax.swing.BorderFactory;
import javax.swing.Box;
import javax.swing.BoxLayout;
import javax.swing.GroupLayout;
import javax.swing.JButton;
import javax.swing.JCheckBox;
import javax.swing.JComboBox;
import javax.swing.JLabel;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JTextField;
import jadx.gui.settings.ExportProjectProperties;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import jadx.core.export.ExportGradle;
import jadx.core.export.ExportGradleType;
import jadx.core.utils.files.FileUtils;
import jadx.gui.JadxWrapper;
import jadx.gui.ui.MainWindow;
import jadx.gui.ui.dialog.CommonDialog;
import jadx.gui.ui.filedialog.FileDialogWrapper;
import jadx.gui.ui.filedialog.FileOpenMode;
import jadx.gui.utils.NLS;
@@ -29,6 +35,7 @@ import jadx.gui.utils.TextStandardActions;
import jadx.gui.utils.ui.DocumentUpdateListener;
public class ExportProjectDialog extends CommonDialog {
private static final Logger LOG = LoggerFactory.getLogger(ExportProjectDialog.class);
private final ExportProjectProperties exportProjectProperties = new ExportProjectProperties();
private final Consumer<ExportProjectProperties> exportListener;
@@ -40,31 +47,26 @@ public class ExportProjectDialog extends CommonDialog {
}
private void initUI() {
JPanel contentPane = makeContentPane();
JPanel buttonPane = initButtonsPanel();
Container container = getContentPane();
container.add(contentPane, BorderLayout.CENTER);
container.add(buttonPane, BorderLayout.PAGE_END);
JPanel contentPanel = new JPanel();
contentPanel.setLayout(new BorderLayout(5, 5));
contentPanel.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10));
contentPanel.add(makeContentPane(), BorderLayout.PAGE_START);
contentPanel.add(initButtonsPanel(), BorderLayout.PAGE_END);
getContentPane().add(contentPanel);
setTitle(NLS.str("export_dialog.title"));
commonWindowInit();
}
private JPanel makeContentPane() {
// top layout
JLabel label = new JLabel(NLS.str("export_dialog.save_path"));
JLabel pathLbl = new JLabel(NLS.str("export_dialog.save_path"));
JTextField pathField = new JTextField();
pathField.setText(mainWindow.getSettings().getLastSaveFilePath().toString());
pathField.getDocument().addDocumentListener(new DocumentUpdateListener(ev -> setExportProjectPath(pathField)));
pathField.setText(mainWindow.getSettings().getLastSaveFilePath().toString());
TextStandardActions.attach(pathField);
JButton browseButton = makeEditorBrowseButton(pathField);
// check box layout
JPanel exportOptionsPanel = new JPanel();
exportOptionsPanel.setBorder(BorderFactory.createTitledBorder(NLS.str("export_dialog.export_options")));
exportOptionsPanel.setLayout(new BoxLayout(exportOptionsPanel, BoxLayout.PAGE_AXIS));
JCheckBox resourceDecode = new JCheckBox(NLS.str("preferences.skipResourcesDecode"));
resourceDecode.setSelected(mainWindow.getSettings().isSkipResources());
resourceDecode.addItemListener(e -> {
@@ -77,55 +79,70 @@ public class ExportProjectDialog extends CommonDialog {
exportProjectProperties.setSkipSources(e.getStateChange() == ItemEvent.SELECTED);
});
JLabel exportTypeLbl = new JLabel(NLS.str("export_dialog.export_gradle_type"));
JComboBox<ExportGradleType> exportTypeComboBox = new JComboBox<>(ExportGradleType.values());
exportTypeLbl.setLabelFor(exportTypeComboBox);
ExportGradleType initialExportType = getExportGradleType();
exportProjectProperties.setExportGradleType(initialExportType);
exportTypeComboBox.setSelectedItem(initialExportType);
exportTypeComboBox.addItemListener(e -> {
exportProjectProperties.setExportGradleType((ExportGradleType) e.getItem());
});
exportTypeComboBox.setEnabled(false);
JCheckBox exportAsGradleProject = new JCheckBox(NLS.str("export_dialog.export_gradle"));
exportAsGradleProject.addItemListener(e -> {
boolean isSelected = e.getStateChange() == ItemEvent.SELECTED;
exportProjectProperties.setAsGradleMode(isSelected);
resourceDecode.setEnabled(!isSelected);
skipSources.setEnabled(!isSelected);
boolean enableGradle = e.getStateChange() == ItemEvent.SELECTED;
exportProjectProperties.setAsGradleMode(enableGradle);
exportTypeComboBox.setEnabled(enableGradle);
resourceDecode.setEnabled(!enableGradle);
skipSources.setEnabled(!enableGradle);
});
JPanel pathPanel = new JPanel();
pathPanel.setLayout(new BoxLayout(pathPanel, BoxLayout.LINE_AXIS));
pathPanel.setAlignmentX(LEFT_ALIGNMENT);
pathPanel.add(pathLbl);
pathPanel.add(Box.createRigidArea(new Dimension(5, 0)));
pathPanel.add(pathField);
pathPanel.add(Box.createRigidArea(new Dimension(5, 0)));
pathPanel.add(browseButton);
JPanel typePanel = new JPanel();
typePanel.setLayout(new BoxLayout(typePanel, BoxLayout.LINE_AXIS));
typePanel.setAlignmentX(LEFT_ALIGNMENT);
typePanel.add(Box.createRigidArea(new Dimension(20, 0)));
typePanel.add(exportTypeLbl);
typePanel.add(Box.createRigidArea(new Dimension(5, 0)));
typePanel.add(exportTypeComboBox);
typePanel.add(Box.createHorizontalGlue());
JPanel exportOptionsPanel = new JPanel();
exportOptionsPanel.setBorder(BorderFactory.createTitledBorder(NLS.str("export_dialog.export_options")));
exportOptionsPanel.setLayout(new BoxLayout(exportOptionsPanel, BoxLayout.PAGE_AXIS));
exportOptionsPanel.add(exportAsGradleProject);
exportOptionsPanel.add(typePanel);
exportOptionsPanel.add(resourceDecode);
exportOptionsPanel.add(skipSources);
// build group box layout
JPanel groupBoxPanel = new JPanel();
GroupLayout groupBoxLayout = new GroupLayout(groupBoxPanel);
groupBoxLayout.setAutoCreateGaps(true);
groupBoxLayout.setAutoCreateContainerGaps(true);
groupBoxPanel.setLayout(groupBoxLayout);
groupBoxLayout.setHorizontalGroup(groupBoxLayout.createParallelGroup()
.addComponent(exportOptionsPanel, GroupLayout.DEFAULT_SIZE, GroupLayout.DEFAULT_SIZE, Integer.MAX_VALUE));
groupBoxLayout.setVerticalGroup(groupBoxLayout.createSequentialGroup()
.addComponent(exportOptionsPanel));
// main layout
JPanel mainPanel = new JPanel();
GroupLayout layout = new GroupLayout(mainPanel);
mainPanel.setLayout(layout);
layout.setAutoCreateGaps(true);
layout.setAutoCreateContainerGaps(true);
// arrange components using GroupLayout
layout.setHorizontalGroup(layout.createParallelGroup(GroupLayout.Alignment.LEADING)
.addGroup(layout.createSequentialGroup()
.addComponent(label)
.addComponent(pathField)
.addComponent(browseButton))
.addComponent(groupBoxPanel));
layout.setVerticalGroup(layout.createSequentialGroup()
.addGroup(layout.createParallelGroup(GroupLayout.Alignment.BASELINE)
.addComponent(label)
.addComponent(pathField)
.addComponent(browseButton))
.addComponent(groupBoxPanel));
mainPanel.setLayout(new BoxLayout(mainPanel, BoxLayout.PAGE_AXIS));
mainPanel.add(pathPanel);
mainPanel.add(Box.createRigidArea(new Dimension(0, 10)));
mainPanel.add(exportOptionsPanel);
return mainPanel;
}
private ExportGradleType getExportGradleType() {
try {
JadxWrapper wrapper = mainWindow.getWrapper();
return ExportGradle.detectExportType(wrapper.getRootNode(), wrapper.getResources());
} catch (Exception e) {
LOG.warn("Failed to detect export type", e);
return ExportGradleType.AUTO;
}
}
private void setExportProjectPath(JTextField field) {
String path = field.getText();
if (!path.isEmpty()) {
@@ -143,8 +160,6 @@ public class ExportProjectDialog extends CommonDialog {
JPanel buttonPane = new JPanel();
buttonPane.setLayout(new BoxLayout(buttonPane, BoxLayout.LINE_AXIS));
buttonPane.setBorder(BorderFactory.createEmptyBorder(0, 10, 10, 10));
buttonPane.add(Box.createRigidArea(new Dimension(10, 0)));
buttonPane.add(Box.createHorizontalGlue());
buttonPane.add(exportProjectButton);
buttonPane.add(Box.createRigidArea(new Dimension(10, 0)));
@@ -168,12 +183,33 @@ public class ExportProjectDialog extends CommonDialog {
}
private void exportProject() {
if (!new File(exportProjectProperties.getExportPath()).exists()) {
String exportPathStr = exportProjectProperties.getExportPath();
if (!validateAndMakeDir(exportPathStr)) {
JOptionPane.showMessageDialog(this, NLS.str("message.enter_valid_path"),
NLS.str("message.errorTitle"), JOptionPane.WARNING_MESSAGE);
return;
}
mainWindow.getSettings().setLastSaveFilePath(Path.of(exportPathStr));
LOG.debug("Export properties: {}", exportProjectProperties);
exportListener.accept(exportProjectProperties);
dispose();
}
private static boolean validateAndMakeDir(String exportPath) {
if (exportPath == null || exportPath.isBlank()) {
return false;
}
try {
Path path = Path.of(exportPath);
if (Files.isRegularFile(path)) {
// dir exists as a file
return false;
}
FileUtils.makeDirs(path);
return true;
} catch (Exception e) {
LOG.warn("Export path validate error, path string:{}", exportPath, e);
return false;
}
}
}
@@ -1,9 +1,14 @@
package jadx.gui.settings;
package jadx.gui.ui.export;
import org.jetbrains.annotations.Nullable;
import jadx.core.export.ExportGradleType;
public class ExportProjectProperties {
private boolean skipSources;
private boolean skipResources;
private boolean asGradleMode;
private @Nullable ExportGradleType exportGradleType;
private String exportPath;
public boolean isSkipSources() {
@@ -30,6 +35,14 @@ public class ExportProjectProperties {
this.asGradleMode = asGradleMode;
}
public @Nullable ExportGradleType getExportGradleType() {
return exportGradleType;
}
public void setExportGradleType(@Nullable ExportGradleType exportGradleType) {
this.exportGradleType = exportGradleType;
}
public String getExportPath() {
return exportPath;
}
@@ -37,4 +50,14 @@ public class ExportProjectProperties {
public void setExportPath(String exportPath) {
this.exportPath = exportPath;
}
@Override
public String toString() {
return "ExportProjectProperties{exportPath='" + exportPath + '\''
+ ", asGradleMode=" + asGradleMode
+ ", exportGradleType=" + exportGradleType
+ ", skipSources=" + skipSources
+ ", skipResources=" + skipResources
+ '}';
}
}
@@ -180,6 +180,7 @@ export_dialog.save_path=Speicherpfad:
export_dialog.browse=Durchsuchen
export_dialog.export_options=Exportoptionen
export_dialog.export_gradle=Als Gradle-Projekt exportieren
#export_dialog.export_gradle_type=Gradle template:
log_viewer.title=Protokollanzeige
log_viewer.log_level=Protokollstufe:
@@ -175,11 +175,12 @@ comment_dialog.usage=Use 'Shift + Enter' to start a new line
rename_dialog.class_help=Enter full name to move class to another package. Start with '.' to move to default (empty) package
export_dialog.title=Export to source code
export_dialog.title=Export
export_dialog.save_path=Save path:
export_dialog.browse=Browse
export_dialog.export_options=Export options
export_dialog.export_gradle=Export as gradle project
export_dialog.export_gradle=Export as a Gradle project
export_dialog.export_gradle_type=Gradle template:
log_viewer.title=Log Viewer
log_viewer.log_level=Log level:
@@ -180,6 +180,7 @@ usage_dialog.label=Usage for:
#export_dialog.browse=Browse
#export_dialog.export_options=Export options
#export_dialog.export_gradle=Export as gradle project
#export_dialog.export_gradle_type=Gradle template:
log_viewer.title=Visor log
log_viewer.log_level=Nivel log:
@@ -180,6 +180,7 @@ comment_dialog.usage=Gunakan Shift + Enter untuk memulai baris baru
#export_dialog.browse=Browse
#export_dialog.export_options=Export options
#export_dialog.export_gradle=Export as gradle project
#export_dialog.export_gradle_type=Gradle template:
log_viewer.title=Pemantau Log
log_viewer.log_level=Tingkat log:
@@ -180,6 +180,7 @@ comment_dialog.usage=Shift + Enter 를 입력해 새 라인에 입력
#export_dialog.browse=Browse
#export_dialog.export_options=Export options
#export_dialog.export_gradle=Export as gradle project
#export_dialog.export_gradle_type=Gradle template:
log_viewer.title=로그 뷰어
log_viewer.log_level=로그 레벨:
@@ -180,6 +180,7 @@ comment_dialog.usage=Use Shift + Enter para pular uma linha
#export_dialog.browse=Browse
#export_dialog.export_options=Export options
#export_dialog.export_gradle=Export as gradle project
#export_dialog.export_gradle_type=Gradle template:
log_viewer.title=Visualizador de log
log_viewer.log_level=Nível do log:
@@ -180,6 +180,7 @@ rename_dialog.class_help=Введите полный путь к пакету,
#export_dialog.browse=Browse
#export_dialog.export_options=Export options
#export_dialog.export_gradle=Export as gradle project
#export_dialog.export_gradle_type=Gradle template:
log_viewer.title=Просмотр логов
log_viewer.log_level=Уровень лога:
@@ -180,6 +180,7 @@ export_dialog.save_path=保存路径:
export_dialog.browse=浏览
export_dialog.export_options=导出选项
export_dialog.export_gradle=导出为 Gradle 项目
#export_dialog.export_gradle_type=Gradle template:
log_viewer.title=日志查看器
log_viewer.log_level=日志等级:
@@ -180,6 +180,7 @@ export_dialog.save_path=儲存路徑:
export_dialog.browse=瀏覽
export_dialog.export_options=匯出選項
export_dialog.export_gradle=匯出成 Gradle 專案
#export_dialog.export_gradle_type=Gradle template:
log_viewer.title=記錄檔檢視器
log_viewer.log_level=記錄層級: