diff --git a/jadx-core/src/main/java/jadx/api/plugins/JadxPluginInfo.java b/jadx-core/src/main/java/jadx/api/plugins/JadxPluginInfo.java index e1d1b37a2..08aa34ede 100644 --- a/jadx-core/src/main/java/jadx/api/plugins/JadxPluginInfo.java +++ b/jadx-core/src/main/java/jadx/api/plugins/JadxPluginInfo.java @@ -4,20 +4,26 @@ public class JadxPluginInfo { private final String pluginId; private final String name; private final String description; + private final String homepage; /** - * Conflicting plugins should have same 'provides' property, only one will be loaded + * Conflicting plugins should have the same 'provides' property; only one will be loaded */ private final String provides; public JadxPluginInfo(String id, String name, String description) { - this(id, name, description, id); + this(id, name, description, "", id); } public JadxPluginInfo(String pluginId, String name, String description, String provides) { + this(pluginId, name, description, "", provides); + } + + public JadxPluginInfo(String pluginId, String name, String description, String homepage, String provides) { this.pluginId = pluginId; this.name = name; this.description = description; + this.homepage = homepage; this.provides = provides; } @@ -33,6 +39,10 @@ public class JadxPluginInfo { return description; } + public String getHomepage() { + return homepage; + } + public String getProvides() { return provides; } diff --git a/jadx-core/src/main/java/jadx/api/plugins/JadxPluginInfoBuilder.java b/jadx-core/src/main/java/jadx/api/plugins/JadxPluginInfoBuilder.java new file mode 100644 index 000000000..10d1bb43a --- /dev/null +++ b/jadx-core/src/main/java/jadx/api/plugins/JadxPluginInfoBuilder.java @@ -0,0 +1,51 @@ +package jadx.api.plugins; + +import java.util.Objects; + +import org.jetbrains.annotations.Nullable; + +public class JadxPluginInfoBuilder { + private String pluginId; + private String name; + private String description; + private String homepage = ""; + private @Nullable String provides; + + public JadxPluginInfoBuilder() { + } + + public JadxPluginInfoBuilder pluginId(String pluginId) { + this.pluginId = Objects.requireNonNull(pluginId); + return this; + } + + public JadxPluginInfoBuilder name(String name) { + this.name = Objects.requireNonNull(name); + return this; + } + + public JadxPluginInfoBuilder description(String description) { + this.description = Objects.requireNonNull(description); + return this; + } + + public JadxPluginInfoBuilder homepage(String homepage) { + this.homepage = homepage; + return this; + } + + public JadxPluginInfoBuilder provides(String provides) { + this.provides = provides; + return this; + } + + public JadxPluginInfo build() { + Objects.requireNonNull(pluginId, "PluginId is required"); + Objects.requireNonNull(name, "Name is required"); + Objects.requireNonNull(description, "Description is required"); + if (provides == null) { + provides = pluginId; + } + return new JadxPluginInfo(pluginId, name, description, homepage, provides); + } +} diff --git a/jadx-core/src/main/java/jadx/api/plugins/events/JadxEvents.java b/jadx-core/src/main/java/jadx/api/plugins/events/JadxEvents.java index fde1ef54b..e9b69815b 100644 --- a/jadx-core/src/main/java/jadx/api/plugins/events/JadxEvents.java +++ b/jadx-core/src/main/java/jadx/api/plugins/events/JadxEvents.java @@ -1,6 +1,7 @@ package jadx.api.plugins.events; import jadx.api.plugins.events.types.NodeRenamedByUser; +import jadx.api.plugins.events.types.ReloadProject; import jadx.api.plugins.events.types.ReloadSettingsWindow; import jadx.api.plugins.gui.ISettingsGroup; import jadx.api.plugins.gui.JadxGuiSettings; @@ -13,13 +14,18 @@ import static jadx.api.plugins.events.JadxEventType.create; public class JadxEvents { /** - * Notify about renames done by user (GUI only). + * Notify about renaming done by user (GUI only). */ public static final JadxEventType NODE_RENAMED_BY_USER = create(); /** - * Request reload of settings window (GUI only). - * Useful for reload custom settings group set with + * Request reload of a current project (GUI only). + */ + public static final JadxEventType RELOAD_PROJECT = create(); + + /** + * Request reload of a settings window (GUI only). + * Useful for a reload custom settings group which was set with * {@link JadxGuiSettings#setCustomSettingsGroup(ISettingsGroup)}. */ public static final JadxEventType RELOAD_SETTINGS_WINDOW = create(); diff --git a/jadx-core/src/main/java/jadx/api/plugins/events/types/ReloadProject.java b/jadx-core/src/main/java/jadx/api/plugins/events/types/ReloadProject.java new file mode 100644 index 000000000..79c9e91e7 --- /dev/null +++ b/jadx-core/src/main/java/jadx/api/plugins/events/types/ReloadProject.java @@ -0,0 +1,24 @@ +package jadx.api.plugins.events.types; + +import jadx.api.plugins.events.IJadxEvent; +import jadx.api.plugins.events.JadxEventType; +import jadx.api.plugins.events.JadxEvents; + +public class ReloadProject implements IJadxEvent { + + public static final ReloadProject INSTANCE = new ReloadProject(); + + private ReloadProject() { + // singleton + } + + @Override + public JadxEventType getType() { + return JadxEvents.RELOAD_PROJECT; + } + + @Override + public String toString() { + return "RELOAD_PROJECT"; + } +} diff --git a/jadx-gui/src/main/java/jadx/gui/plugins/context/GuiSettingsContext.java b/jadx-gui/src/main/java/jadx/gui/plugins/context/GuiSettingsContext.java index 153676b9c..492e60fd2 100644 --- a/jadx-gui/src/main/java/jadx/gui/plugins/context/GuiSettingsContext.java +++ b/jadx-gui/src/main/java/jadx/gui/plugins/context/GuiSettingsContext.java @@ -6,7 +6,7 @@ import jadx.api.plugins.gui.ISettingsGroup; import jadx.api.plugins.gui.JadxGuiSettings; import jadx.api.plugins.options.OptionDescription; import jadx.gui.settings.ui.SubSettingsGroup; -import jadx.gui.settings.ui.plugins.PluginsSettings; +import jadx.gui.settings.ui.plugins.PluginSettings; import jadx.gui.ui.MainWindow; public class GuiSettingsContext implements JadxGuiSettings { @@ -24,7 +24,7 @@ public class GuiSettingsContext implements JadxGuiSettings { @Override public ISettingsGroup buildSettingsGroupForOptions(String title, List options) { MainWindow mainWindow = guiPluginContext.getCommonContext().getMainWindow(); - PluginsSettings pluginsSettings = new PluginsSettings(mainWindow, mainWindow.getSettings()); + PluginSettings pluginsSettings = new PluginSettings(mainWindow, mainWindow.getSettings()); SubSettingsGroup settingsGroup = new SubSettingsGroup(title); pluginsSettings.addOptions(settingsGroup, options); return settingsGroup; diff --git a/jadx-gui/src/main/java/jadx/gui/plugins/quark/QuarkManager.java b/jadx-gui/src/main/java/jadx/gui/plugins/quark/QuarkManager.java index 988a206b6..59f534fcb 100644 --- a/jadx-gui/src/main/java/jadx/gui/plugins/quark/QuarkManager.java +++ b/jadx-gui/src/main/java/jadx/gui/plugins/quark/QuarkManager.java @@ -203,7 +203,7 @@ public class QuarkManager { } private void runCommand(List cmd) throws Exception { - UiUtils.uiRun(() -> mainWindow.showLogViewer(LogOptions.forLevel(Level.INFO))); + mainWindow.showLogViewer(LogOptions.forLevel(Level.INFO)); LOG.info("Running command: {}", String.join(" ", cmd)); ProcessBuilder builder = new ProcessBuilder(cmd); builder.redirectErrorStream(true); diff --git a/jadx-gui/src/main/java/jadx/gui/settings/ui/JadxSettingsWindow.java b/jadx-gui/src/main/java/jadx/gui/settings/ui/JadxSettingsWindow.java index 44d40d053..2644f07e9 100644 --- a/jadx-gui/src/main/java/jadx/gui/settings/ui/JadxSettingsWindow.java +++ b/jadx-gui/src/main/java/jadx/gui/settings/ui/JadxSettingsWindow.java @@ -37,7 +37,6 @@ import javax.swing.ScrollPaneConstants; import javax.swing.SpinnerNumberModel; import javax.swing.SwingUtilities; import javax.swing.WindowConstants; -import javax.swing.tree.TreePath; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -61,7 +60,7 @@ import jadx.gui.settings.JadxSettings; import jadx.gui.settings.JadxSettingsAdapter; import jadx.gui.settings.LineNumbersMode; import jadx.gui.settings.ui.cache.CacheSettingsGroup; -import jadx.gui.settings.ui.plugins.PluginsSettings; +import jadx.gui.settings.ui.plugins.PluginSettings; import jadx.gui.settings.ui.shortcut.ShortcutsSettingsGroup; import jadx.gui.ui.MainWindow; import jadx.gui.ui.codearea.EditorTheme; @@ -85,7 +84,7 @@ public class JadxSettingsWindow extends JDialog { private final transient LangLocale prevLang; private transient boolean needReload = false; - private SettingsTree tree; + private transient SettingsTree tree; public JadxSettingsWindow(MainWindow mainWindow, JadxSettings settings) { this.mainWindow = mainWindow; @@ -95,7 +94,6 @@ public class JadxSettingsWindow extends JDialog { this.prevLang = settings.getLangLocale(); initUI(); - mainWindow.events().addListener(JadxEvents.RELOAD_SETTINGS_WINDOW, r -> UiUtils.uiRun(this::reloadUI)); setTitle(NLS.str("preferences.title")); setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE); @@ -106,16 +104,19 @@ public class JadxSettingsWindow extends JDialog { if (!mainWindow.getSettings().loadWindowPos(this)) { setSize(700, 800); } + mainWindow.events().addListener(JadxEvents.RELOAD_SETTINGS_WINDOW, r -> UiUtils.uiRun(this::reloadUI)); + mainWindow.events().addListener(JadxEvents.RELOAD_PROJECT, r -> UiUtils.uiRun(this::reloadUI)); } private void reloadUI() { - TreePath selectionPath = tree.getSelectionPath(); - mainWindow.getSettings().saveWindowPos(this); + int[] selection = tree.getSelectionRows(); getContentPane().removeAll(); initUI(); - pack(); - mainWindow.getSettings().loadWindowPos(this); - tree.setSelectionPath(selectionPath); + // wait for other events to process + UiUtils.uiRun(() -> { + tree.setSelectionRows(selection); + SwingUtilities.updateComponentTreeUI(this); + }); } private void initUI() { @@ -130,7 +131,7 @@ public class JadxSettingsWindow extends JDialog { groups.add(new ShortcutsSettingsGroup(this, settings)); groups.add(makeSearchResGroup()); groups.add(makeProjectGroup()); - groups.add(new PluginsSettings(mainWindow, settings).build()); + groups.add(new PluginSettings(mainWindow, settings).build()); groups.add(makeOtherGroup()); tree = new SettingsTree(); diff --git a/jadx-gui/src/main/java/jadx/gui/settings/ui/plugins/AvailablePluginNode.java b/jadx-gui/src/main/java/jadx/gui/settings/ui/plugins/AvailablePluginNode.java new file mode 100644 index 000000000..563e58596 --- /dev/null +++ b/jadx-gui/src/main/java/jadx/gui/settings/ui/plugins/AvailablePluginNode.java @@ -0,0 +1,47 @@ +package jadx.gui.settings.ui.plugins; + +import jadx.plugins.tools.data.JadxPluginMetadata; + +public class AvailablePluginNode extends BasePluginListNode { + + private final JadxPluginMetadata metadata; + + public AvailablePluginNode(JadxPluginMetadata metadata) { + this.metadata = metadata; + } + + @Override + public String getTitle() { + return metadata.getName(); + } + + @Override + public boolean hasDetails() { + return true; + } + + @Override + public String getPluginId() { + return metadata.getPluginId(); + } + + @Override + public String getDescription() { + return metadata.getDescription(); + } + + @Override + public String getHomepage() { + return metadata.getHomepage(); + } + + @Override + public String getLocationId() { + return metadata.getLocationId(); + } + + @Override + public PluginAction getAction() { + return PluginAction.INSTALL; + } +} diff --git a/jadx-gui/src/main/java/jadx/gui/settings/ui/plugins/BasePluginListNode.java b/jadx-gui/src/main/java/jadx/gui/settings/ui/plugins/BasePluginListNode.java index 2aa3b61a2..34eac9268 100644 --- a/jadx-gui/src/main/java/jadx/gui/settings/ui/plugins/BasePluginListNode.java +++ b/jadx-gui/src/main/java/jadx/gui/settings/ui/plugins/BasePluginListNode.java @@ -2,19 +2,33 @@ package jadx.gui.settings.ui.plugins; import org.jetbrains.annotations.Nullable; -import jadx.api.plugins.JadxPluginInfo; - abstract class BasePluginListNode { - public @Nullable String getTitle() { + public abstract String getTitle(); + + public abstract boolean hasDetails(); + + public String getPluginId() { return null; } - public JadxPluginInfo getPluginInfo() { + public String getDescription() { + return null; + } + + public String getHomepage() { + return null; + } + + public @Nullable String getLocationId() { return null; } public @Nullable String getVersion() { return null; } + + public PluginAction getAction() { + return PluginAction.NONE; + } } diff --git a/jadx-gui/src/main/java/jadx/gui/settings/ui/plugins/InstallPluginDialog.java b/jadx-gui/src/main/java/jadx/gui/settings/ui/plugins/InstallPluginDialog.java index 9107de12f..eed865b11 100644 --- a/jadx-gui/src/main/java/jadx/gui/settings/ui/plugins/InstallPluginDialog.java +++ b/jadx-gui/src/main/java/jadx/gui/settings/ui/plugins/InstallPluginDialog.java @@ -22,29 +22,25 @@ import org.slf4j.LoggerFactory; import com.formdev.flatlaf.FlatClientProperties; -import ch.qos.logback.classic.Level; - -import jadx.api.plugins.events.types.ReloadSettingsWindow; -import jadx.gui.logs.LogOptions; import jadx.gui.ui.MainWindow; import jadx.gui.ui.filedialog.FileDialogWrapper; import jadx.gui.ui.filedialog.FileOpenMode; import jadx.gui.utils.NLS; import jadx.gui.utils.TextStandardActions; import jadx.gui.utils.UiUtils; -import jadx.plugins.tools.JadxPluginsTools; -import jadx.plugins.tools.data.JadxPluginMetadata; public class InstallPluginDialog extends JDialog { private static final Logger LOG = LoggerFactory.getLogger(InstallPluginDialog.class); private static final long serialVersionUID = 5304314264730563853L; private final MainWindow mainWindow; + private final PluginSettings pluginsSettings; private JTextField locationFld; - public InstallPluginDialog(MainWindow mainWindow) { + public InstallPluginDialog(MainWindow mainWindow, PluginSettings pluginsSettings) { super(mainWindow, NLS.str("preferences.plugins.install")); this.mainWindow = mainWindow; + this.pluginsSettings = pluginsSettings; init(); } @@ -121,20 +117,7 @@ public class InstallPluginDialog extends JDialog { } private void install() { - mainWindow.getBackgroundExecutor().execute(NLS.str("preferences.plugins.task.installing"), - () -> { - try { - JadxPluginMetadata metadata = JadxPluginsTools.getInstance().install(locationFld.getText()); - LOG.info("Plugin installed: {}", metadata); - } catch (Exception e) { - LOG.error("Install failed", e); - mainWindow.showLogViewer(LogOptions.forLevel(Level.ERROR)); - } - }, - status -> { - mainWindow.events().send(ReloadSettingsWindow.INSTANCE); - UiUtils.uiRun(mainWindow::reopen); - }); + pluginsSettings.install(locationFld.getText()); dispose(); } } diff --git a/jadx-gui/src/main/java/jadx/gui/settings/ui/plugins/InstalledPluginNode.java b/jadx-gui/src/main/java/jadx/gui/settings/ui/plugins/InstalledPluginNode.java new file mode 100644 index 000000000..bd2ec630e --- /dev/null +++ b/jadx-gui/src/main/java/jadx/gui/settings/ui/plugins/InstalledPluginNode.java @@ -0,0 +1,62 @@ +package jadx.gui.settings.ui.plugins; + +import org.jetbrains.annotations.Nullable; + +import jadx.core.plugins.PluginContext; +import jadx.plugins.tools.data.JadxPluginMetadata; + +public class InstalledPluginNode extends BasePluginListNode { + private final PluginContext plugin; + private final JadxPluginMetadata metadata; + + public InstalledPluginNode(PluginContext plugin, @Nullable JadxPluginMetadata metadata) { + this.plugin = plugin; + this.metadata = metadata; + } + + @Override + public @Nullable String getTitle() { + return plugin.getPluginInfo().getName(); + } + + @Override + public boolean hasDetails() { + return true; + } + + @Override + public String getPluginId() { + return plugin.getPluginId(); + } + + @Override + public String getDescription() { + return plugin.getPluginInfo().getDescription(); + } + + @Override + public String getHomepage() { + return plugin.getPluginInfo().getHomepage(); + } + + @Override + public PluginAction getAction() { + if (metadata != null) { + return PluginAction.UNINSTALL; + } + return PluginAction.NONE; + } + + @Override + public @Nullable String getVersion() { + if (metadata != null) { + return metadata.getVersion(); + } + return null; + } + + @Override + public String toString() { + return plugin.getPluginInfo().getName(); + } +} diff --git a/jadx-gui/src/main/java/jadx/gui/settings/ui/plugins/PluginAction.java b/jadx-gui/src/main/java/jadx/gui/settings/ui/plugins/PluginAction.java new file mode 100644 index 000000000..897890b50 --- /dev/null +++ b/jadx-gui/src/main/java/jadx/gui/settings/ui/plugins/PluginAction.java @@ -0,0 +1,7 @@ +package jadx.gui.settings.ui.plugins; + +public enum PluginAction { + NONE, + INSTALL, + UNINSTALL +} diff --git a/jadx-gui/src/main/java/jadx/gui/settings/ui/plugins/PluginListNode.java b/jadx-gui/src/main/java/jadx/gui/settings/ui/plugins/PluginListNode.java deleted file mode 100644 index bd5a37b8f..000000000 --- a/jadx-gui/src/main/java/jadx/gui/settings/ui/plugins/PluginListNode.java +++ /dev/null @@ -1,39 +0,0 @@ -package jadx.gui.settings.ui.plugins; - -import org.jetbrains.annotations.Nullable; - -import jadx.api.plugins.JadxPluginInfo; -import jadx.core.plugins.PluginContext; -import jadx.plugins.tools.data.JadxPluginMetadata; - -public class PluginListNode extends BasePluginListNode { - private final PluginContext plugin; - private final JadxPluginMetadata metadata; - - public PluginListNode(PluginContext plugin, @Nullable JadxPluginMetadata metadata) { - this.plugin = plugin; - this.metadata = metadata; - } - - @Override - public JadxPluginInfo getPluginInfo() { - return plugin.getPluginInfo(); - } - - public @Nullable JadxPluginMetadata getMetadata() { - return metadata; - } - - @Override - public @Nullable String getVersion() { - if (metadata != null) { - return metadata.getVersion(); - } - return null; - } - - @Override - public String toString() { - return plugin.getPluginInfo().getName(); - } -} diff --git a/jadx-gui/src/main/java/jadx/gui/settings/ui/plugins/PluginsSettings.java b/jadx-gui/src/main/java/jadx/gui/settings/ui/plugins/PluginSettings.java similarity index 84% rename from jadx-gui/src/main/java/jadx/gui/settings/ui/plugins/PluginsSettings.java rename to jadx-gui/src/main/java/jadx/gui/settings/ui/plugins/PluginSettings.java index b259e3959..6ee94abfa 100644 --- a/jadx-gui/src/main/java/jadx/gui/settings/ui/plugins/PluginsSettings.java +++ b/jadx-gui/src/main/java/jadx/gui/settings/ui/plugins/PluginSettings.java @@ -17,7 +17,9 @@ import javax.swing.JTextField; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import jadx.api.plugins.events.types.ReloadSettingsWindow; +import ch.qos.logback.classic.Level; + +import jadx.api.plugins.events.types.ReloadProject; import jadx.api.plugins.gui.ISettingsGroup; import jadx.api.plugins.gui.JadxGuiContext; import jadx.api.plugins.options.JadxPluginOptions; @@ -26,33 +28,34 @@ import jadx.api.plugins.options.OptionFlag; import jadx.api.plugins.options.OptionType; import jadx.core.plugins.PluginContext; import jadx.core.utils.Utils; +import jadx.gui.logs.LogOptions; import jadx.gui.plugins.context.GuiPluginContext; import jadx.gui.settings.JadxProject; import jadx.gui.settings.JadxSettings; import jadx.gui.settings.ui.SettingsGroup; import jadx.gui.ui.MainWindow; import jadx.gui.utils.NLS; -import jadx.gui.utils.UiUtils; import jadx.gui.utils.plugins.CollectPlugins; import jadx.gui.utils.ui.DocumentUpdateListener; import jadx.plugins.tools.JadxPluginsTools; +import jadx.plugins.tools.data.JadxPluginMetadata; import jadx.plugins.tools.data.JadxPluginUpdate; -public class PluginsSettings { - private static final Logger LOG = LoggerFactory.getLogger(PluginsSettings.class); +public class PluginSettings { + private static final Logger LOG = LoggerFactory.getLogger(PluginSettings.class); private final MainWindow mainWindow; private final JadxSettings settings; - public PluginsSettings(MainWindow mainWindow, JadxSettings settings) { + public PluginSettings(MainWindow mainWindow, JadxSettings settings) { this.mainWindow = mainWindow; this.settings = settings; } public ISettingsGroup build() { - List list = new CollectPlugins(mainWindow).build(); - ISettingsGroup pluginsGroup = new PluginsSettingsGroup(this, list); - for (PluginContext context : list) { + List installedPlugins = new CollectPlugins(mainWindow).build(); + ISettingsGroup pluginsGroup = new PluginSettingsGroup(this, mainWindow, installedPlugins); + for (PluginContext context : installedPlugins) { ISettingsGroup pluginGroup = addPluginGroup(context); if (pluginGroup != null) { pluginsGroup.getSubGroups().add(pluginGroup); @@ -62,7 +65,25 @@ public class PluginsSettings { } public void addPlugin() { - new InstallPluginDialog(mainWindow).setVisible(true); + new InstallPluginDialog(mainWindow, this).setVisible(true); + } + + private void requestReload() { + mainWindow.events().send(ReloadProject.INSTANCE); + } + + public void install(String locationId) { + mainWindow.getBackgroundExecutor().execute(NLS.str("preferences.plugins.task.installing"), + () -> { + try { + JadxPluginMetadata metadata = JadxPluginsTools.getInstance().install(locationId); + LOG.info("Plugin installed: {}", metadata); + requestReload(); + } catch (Exception e) { + LOG.error("Install failed", e); + mainWindow.showLogViewer(LogOptions.forLevel(Level.ERROR)); + } + }); } public void uninstall(String pluginId) { @@ -70,8 +91,7 @@ public class PluginsSettings { boolean success = JadxPluginsTools.getInstance().uninstall(pluginId); if (success) { LOG.info("Uninstall complete"); - mainWindow.events().send(ReloadSettingsWindow.INSTANCE); - UiUtils.uiRun(mainWindow::reopen); + requestReload(); } else { LOG.warn("Uninstall failed"); } @@ -83,8 +103,7 @@ public class PluginsSettings { List updates = JadxPluginsTools.getInstance().updateAll(); if (!updates.isEmpty()) { LOG.info("Updates: {}\n ", Utils.listToString(updates, "\n ")); - mainWindow.events().send(ReloadSettingsWindow.INSTANCE); - UiUtils.uiRun(mainWindow::reopen); + requestReload(); } else { LOG.info("No updates found"); } diff --git a/jadx-gui/src/main/java/jadx/gui/settings/ui/plugins/PluginsSettingsGroup.java b/jadx-gui/src/main/java/jadx/gui/settings/ui/plugins/PluginSettingsGroup.java similarity index 62% rename from jadx-gui/src/main/java/jadx/gui/settings/ui/plugins/PluginsSettingsGroup.java rename to jadx-gui/src/main/java/jadx/gui/settings/ui/plugins/PluginSettingsGroup.java index f4a4a1b1d..2cc5254dd 100644 --- a/jadx-gui/src/main/java/jadx/gui/settings/ui/plugins/PluginsSettingsGroup.java +++ b/jadx-gui/src/main/java/jadx/gui/settings/ui/plugins/PluginSettingsGroup.java @@ -9,6 +9,8 @@ import java.util.Comparator; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; import javax.swing.BorderFactory; import javax.swing.Box; @@ -26,27 +28,36 @@ import javax.swing.ListCellRenderer; import javax.swing.ListSelectionModel; import javax.swing.SwingConstants; -import jadx.api.plugins.JadxPluginInfo; +import org.jetbrains.annotations.Nullable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + import jadx.api.plugins.gui.ISettingsGroup; import jadx.core.plugins.PluginContext; +import jadx.core.utils.StringUtils; import jadx.core.utils.Utils; +import jadx.gui.ui.MainWindow; import jadx.gui.utils.NLS; +import jadx.plugins.tools.JadxPluginsList; import jadx.plugins.tools.JadxPluginsTools; import jadx.plugins.tools.data.JadxPluginMetadata; -class PluginsSettingsGroup implements ISettingsGroup { - private final PluginsSettings pluginsSettings; +class PluginSettingsGroup implements ISettingsGroup { + private static final Logger LOG = LoggerFactory.getLogger(PluginSettingsGroup.class); + + private final PluginSettings pluginsSettings; + private final MainWindow mainWindow; private final String title; private final List subGroups = new ArrayList<>(); - private final List pluginsList; + private final List installedPlugins; - private PluginListNode selectedPlugin; private JPanel detailsPanel; - public PluginsSettingsGroup(PluginsSettings pluginsSettings, List pluginsList) { - this.pluginsSettings = pluginsSettings; + public PluginSettingsGroup(PluginSettings pluginSettings, MainWindow mainWindow, List installedPlugins) { + this.pluginsSettings = pluginSettings; + this.mainWindow = mainWindow; this.title = NLS.str("preferences.plugins"); - this.pluginsList = pluginsList; + this.installedPlugins = installedPlugins; } @Override @@ -83,25 +94,27 @@ class PluginsSettingsGroup implements ISettingsGroup { installed.forEach(p -> installedMap.put(p.getPluginId(), p)); List nodes = new ArrayList<>(installed.size() + 3); - for (PluginContext plugin : pluginsList) { - nodes.add(new PluginListNode(plugin, installedMap.get(plugin.getPluginId()))); + for (PluginContext plugin : installedPlugins) { + nodes.add(new InstalledPluginNode(plugin, installedMap.get(plugin.getPluginId()))); } - nodes.sort(Comparator.comparing(n -> n.getPluginInfo().getName())); + nodes.sort(Comparator.comparing(BasePluginListNode::getTitle)); DefaultListModel listModel = new DefaultListModel<>(); listModel.addElement(new TitleNode("Installed")); nodes.stream().filter(n -> n.getVersion() != null).forEach(listModel::addElement); listModel.addElement(new TitleNode("Bundled")); nodes.stream().filter(n -> n.getVersion() == null).forEach(listModel::addElement); - // TODO: load external plugins list - // listModel.addElement(new TitleNode("Available")); + listModel.addElement(new TitleNode("Available")); - JList pluginsList = new JList<>(listModel); - pluginsList.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); - pluginsList.setCellRenderer(new PluginsListCellRenderer(pluginsList)); - pluginsList.addListSelectionListener(ev -> onSelection(pluginsList.getSelectedValue())); + JList pluginList = new JList<>(listModel); + pluginList.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); + pluginList.setCellRenderer(new PluginsListCellRenderer()); + pluginList.addListSelectionListener(ev -> onSelection(pluginList.getSelectedValue())); - JScrollPane scrollPane = new JScrollPane(pluginsList); + loadAvailablePlugins(listModel, installedPlugins); + + JScrollPane scrollPane = new JScrollPane(pluginList); + scrollPane.setMinimumSize(new Dimension(80, 120)); detailsPanel = new JPanel(new BorderLayout(5, 5)); detailsPanel.setBorder(BorderFactory.createCompoundBorder( @@ -113,7 +126,6 @@ class PluginsSettingsGroup implements ISettingsGroup { splitPanel.setBorder(BorderFactory.createEmptyBorder(10, 2, 2, 2)); splitPanel.setLeftComponent(scrollPane); splitPanel.setRightComponent(detailsPanel); - splitPanel.setDividerLocation(0.4); JPanel mainPanel = new JPanel(); mainPanel.setLayout(new BorderLayout(5, 5)); @@ -123,22 +135,45 @@ class PluginsSettingsGroup implements ISettingsGroup { return mainPanel; } + private void loadAvailablePlugins(DefaultListModel listModel, List installedPlugins) { + List list = new ArrayList<>(); + mainWindow.getBackgroundExecutor().execute( + NLS.str("preferences.plugins.task.downloading_list"), + () -> { + List availablePlugins; + try { + availablePlugins = JadxPluginsList.getInstance().fetch(); + } catch (Exception e) { + LOG.warn("Failed to load available plugins list", e); + return; + } + Set installed = installedPlugins.stream().map(PluginContext::getPluginId).collect(Collectors.toSet()); + for (JadxPluginMetadata availablePlugin : availablePlugins) { + if (!installed.contains(availablePlugin.getPluginId())) { + list.add(new AvailablePluginNode(availablePlugin)); + } + } + }, + status -> listModel.addAll(list)); + } + private void onSelection(BasePluginListNode node) { detailsPanel.removeAll(); - JadxPluginInfo pluginInfo = node.getPluginInfo(); - if (pluginInfo != null) { - JButton uninstallBtn = new JButton("Uninstall"); - if (node.getVersion() != null) { - uninstallBtn.addActionListener(ev -> pluginsSettings.uninstall(pluginInfo.getPluginId())); - } else { - uninstallBtn.setEnabled(false); - } - JLabel nameLbl = new JLabel(pluginInfo.getName()); + if (node.hasDetails()) { + JLabel nameLbl = new JLabel(node.getTitle()); Font baseFont = nameLbl.getFont(); nameLbl.setFont(baseFont.deriveFont(Font.BOLD, baseFont.getSize2D() + 2)); + String desc; + String homepage = node.getHomepage(); + if (StringUtils.notBlank(homepage)) { + desc = node.getDescription() + "\n\nHomepage: " + homepage; + } else { + desc = node.getDescription(); + } + JTextPane descArea = new JTextPane(); - descArea.setText(pluginInfo.getDescription()); + descArea.setText(desc); descArea.setFont(baseFont.deriveFont(baseFont.getSize2D() + 1)); descArea.setEditable(false); descArea.setBorder(BorderFactory.createEmptyBorder()); @@ -149,21 +184,41 @@ class PluginsSettingsGroup implements ISettingsGroup { top.setBorder(BorderFactory.createEmptyBorder(10, 2, 10, 2)); top.add(nameLbl); top.add(Box.createHorizontalGlue()); - top.add(uninstallBtn); - + JButton actionBtn = makeActionButton(node); + if (actionBtn != null) { + top.add(actionBtn); + } detailsPanel.add(top, BorderLayout.PAGE_START); detailsPanel.add(descArea, BorderLayout.CENTER); } detailsPanel.updateUI(); } + private @Nullable JButton makeActionButton(BasePluginListNode node) { + switch (node.getAction()) { + case NONE: + return null; + case INSTALL: { + JButton installBtn = new JButton(NLS.str("preferences.plugins.install_btn")); + installBtn.addActionListener(ev -> pluginsSettings.install(node.getLocationId())); + return installBtn; + } + case UNINSTALL: { + JButton uninstallBtn = new JButton(NLS.str("preferences.plugins.uninstall_btn")); + uninstallBtn.addActionListener(ev -> pluginsSettings.uninstall(node.getPluginId())); + return uninstallBtn; + } + } + return null; + } + private static class PluginsListCellRenderer implements ListCellRenderer { private final JPanel panel; private final JLabel nameLbl; private final JLabel versionLbl; private final JLabel titleLbl; - public PluginsListCellRenderer(JList pluginsList) { + public PluginsListCellRenderer() { panel = new JPanel(); panel.setOpaque(true); panel.setLayout(new BoxLayout(panel, BoxLayout.LINE_AXIS)); @@ -176,6 +231,7 @@ class PluginsSettingsGroup implements ISettingsGroup { versionLbl.setOpaque(true); panel.add(nameLbl); + panel.add(Box.createHorizontalStrut(20)); panel.add(Box.createHorizontalGlue()); panel.add(versionLbl); @@ -187,13 +243,12 @@ class PluginsSettingsGroup implements ISettingsGroup { @Override public Component getListCellRendererComponent(JList list, BasePluginListNode value, int index, boolean isSelected, boolean cellHasFocus) { - String title = value.getTitle(); - if (title != null) { - titleLbl.setText(title); + if (!value.hasDetails()) { + titleLbl.setText(value.getTitle()); return titleLbl; } - nameLbl.setText(value.getPluginInfo().getName()); - nameLbl.setToolTipText(value.getPluginInfo().getDescription()); + nameLbl.setText(value.getTitle()); + nameLbl.setToolTipText(value.getLocationId()); versionLbl.setText(Utils.getOrElse(value.getVersion(), "")); if (isSelected) { panel.setBackground(list.getSelectionBackground()); diff --git a/jadx-gui/src/main/java/jadx/gui/settings/ui/plugins/TitleNode.java b/jadx-gui/src/main/java/jadx/gui/settings/ui/plugins/TitleNode.java index d78f71e2c..b50bcba0a 100644 --- a/jadx-gui/src/main/java/jadx/gui/settings/ui/plugins/TitleNode.java +++ b/jadx-gui/src/main/java/jadx/gui/settings/ui/plugins/TitleNode.java @@ -11,4 +11,9 @@ public class TitleNode extends BasePluginListNode { public String getTitle() { return title; } + + @Override + public boolean hasDetails() { + return false; + } } 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 79b5c3cbd..65c40e6c5 100644 --- a/jadx-gui/src/main/java/jadx/gui/ui/MainWindow.java +++ b/jadx-gui/src/main/java/jadx/gui/ui/MainWindow.java @@ -81,6 +81,8 @@ import jadx.api.JavaClass; import jadx.api.JavaNode; import jadx.api.ResourceFile; import jadx.api.plugins.events.IJadxEvents; +import jadx.api.plugins.events.JadxEvents; +import jadx.api.plugins.events.types.ReloadProject; import jadx.api.plugins.utils.CommonFileUtils; import jadx.core.Jadx; import jadx.core.export.TemplateFile; @@ -107,7 +109,7 @@ import jadx.gui.plugins.quark.QuarkDialog; import jadx.gui.settings.JadxProject; import jadx.gui.settings.JadxSettings; import jadx.gui.settings.ui.JadxSettingsWindow; -import jadx.gui.settings.ui.plugins.InstallPluginDialog; +import jadx.gui.settings.ui.plugins.PluginSettings; import jadx.gui.treemodel.ApkSignature; import jadx.gui.treemodel.JClass; import jadx.gui.treemodel.JLoadableNode; @@ -472,12 +474,14 @@ public class MainWindow extends JFrame { return loadedFile.resolveSibling(fileName); } - public synchronized void reopen() { - saveAll(); - closeAll(); - loadFiles(EMPTY_RUNNABLE); + public void reopen() { + synchronized (ReloadProject.INSTANCE) { + saveAll(); + closeAll(); + loadFiles(EMPTY_RUNNABLE); - menuBar.reloadShortcuts(); + menuBar.reloadShortcuts(); + } } private void openProject(Path path, Runnable onFinish) { @@ -576,6 +580,7 @@ public class MainWindow extends JFrame { initTree(); updateLiveReload(project.isEnableLiveReload()); BreakpointManager.init(project.getFilePaths().get(0).toAbsolutePath().getParent()); + events().addListener(JadxEvents.RELOAD_PROJECT, ev -> UiUtils.uiRun(this::reopen)); List openTabs = project.getOpenTabs(this); backgroundExecutor.execute(NLS.str("progress.load"), @@ -1519,11 +1524,13 @@ public class MainWindow extends JFrame { } public void showLogViewer(LogOptions logOptions) { - if (settings.isDockLogViewer()) { - showDockedLog(logOptions); - } else { - LogViewerDialog.open(this, logOptions); - } + UiUtils.uiRun(() -> { + if (settings.isDockLogViewer()) { + showDockedLog(logOptions); + } else { + LogViewerDialog.open(this, logOptions); + } + }); } private void showDockedLog(LogOptions logOptions) { @@ -1555,7 +1562,7 @@ public class MainWindow extends JFrame { public void resetPluginsMenu() { pluginsMenu.removeAll(); - pluginsMenu.add(new ActionHandler(() -> new InstallPluginDialog(this).setVisible(true)) + pluginsMenu.add(new ActionHandler(() -> new PluginSettings(this, settings).addPlugin()) .withNameAndDesc(NLS.str("preferences.plugins.install"))); } diff --git a/jadx-gui/src/main/resources/i18n/Messages_de_DE.properties b/jadx-gui/src/main/resources/i18n/Messages_de_DE.properties index 76fa6ea06..9c71d9566 100644 --- a/jadx-gui/src/main/resources/i18n/Messages_de_DE.properties +++ b/jadx-gui/src/main/resources/i18n/Messages_de_DE.properties @@ -232,6 +232,7 @@ preferences.res_skip_file=Dateien überspringen (MB) #preferences.plugins.install=Install plugin #preferences.plugins.install_btn=Install +#preferences.plugins.uninstall_btn=Uninstall #preferences.plugins.location_id_label=Location id: #preferences.plugins.plugin_jar=Select Plugin jar #preferences.plugins.plugin_jar_label=or @@ -240,6 +241,7 @@ preferences.res_skip_file=Dateien überspringen (MB) #preferences.plugins.task.installing=Installing plugin #preferences.plugins.task.uninstalling=Uninstalling plugin #preferences.plugins.task.updating=Updating plugins +#preferences.plugins.task.downloading_list=Downloading plugins list #preferences.cache=Cache #preferences.cache.location=Cache location 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 4b0fec165..e441e3b55 100644 --- a/jadx-gui/src/main/resources/i18n/Messages_en_US.properties +++ b/jadx-gui/src/main/resources/i18n/Messages_en_US.properties @@ -232,6 +232,7 @@ preferences.res_skip_file=Skip resources files if larger (MB) (0 - disable) preferences.plugins.install=Install plugin preferences.plugins.install_btn=Install +preferences.plugins.uninstall_btn=Uninstall preferences.plugins.location_id_label=Location id: preferences.plugins.plugin_jar=Select Plugin jar preferences.plugins.plugin_jar_label=or @@ -240,6 +241,7 @@ preferences.plugins.details=Plugin details preferences.plugins.task.installing=Installing plugin preferences.plugins.task.uninstalling=Uninstalling plugin preferences.plugins.task.updating=Updating plugins +preferences.plugins.task.downloading_list=Downloading plugins list preferences.cache=Cache preferences.cache.location=Cache location 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 6d2feeb68..0888a1efc 100644 --- a/jadx-gui/src/main/resources/i18n/Messages_es_ES.properties +++ b/jadx-gui/src/main/resources/i18n/Messages_es_ES.properties @@ -232,6 +232,7 @@ preferences.reset_title=Reestablecer preferencias #preferences.plugins.install=Install plugin #preferences.plugins.install_btn=Install +#preferences.plugins.uninstall_btn=Uninstall #preferences.plugins.location_id_label=Location id: #preferences.plugins.plugin_jar=Select Plugin jar #preferences.plugins.plugin_jar_label=or @@ -240,6 +241,7 @@ preferences.reset_title=Reestablecer preferencias #preferences.plugins.task.installing=Installing plugin #preferences.plugins.task.uninstalling=Uninstalling plugin #preferences.plugins.task.updating=Updating plugins +#preferences.plugins.task.downloading_list=Downloading plugins list #preferences.cache=Cache #preferences.cache.location=Cache location diff --git a/jadx-gui/src/main/resources/i18n/Messages_ko_KR.properties b/jadx-gui/src/main/resources/i18n/Messages_ko_KR.properties index 101209d0f..ce7e11274 100644 --- a/jadx-gui/src/main/resources/i18n/Messages_ko_KR.properties +++ b/jadx-gui/src/main/resources/i18n/Messages_ko_KR.properties @@ -232,6 +232,7 @@ preferences.res_skip_file=이 옵션보다 큰 파일 건너 뛰기 (MB) #preferences.plugins.install=Install plugin #preferences.plugins.install_btn=Install +#preferences.plugins.uninstall_btn=Uninstall #preferences.plugins.location_id_label=Location id: #preferences.plugins.plugin_jar=Select Plugin jar #preferences.plugins.plugin_jar_label=or @@ -240,6 +241,7 @@ preferences.res_skip_file=이 옵션보다 큰 파일 건너 뛰기 (MB) #preferences.plugins.task.installing=Installing plugin #preferences.plugins.task.uninstalling=Uninstalling plugin #preferences.plugins.task.updating=Updating plugins +#preferences.plugins.task.downloading_list=Downloading plugins list #preferences.cache=Cache #preferences.cache.location=Cache location diff --git a/jadx-gui/src/main/resources/i18n/Messages_pt_BR.properties b/jadx-gui/src/main/resources/i18n/Messages_pt_BR.properties index 87ee64037..f2260912e 100644 --- a/jadx-gui/src/main/resources/i18n/Messages_pt_BR.properties +++ b/jadx-gui/src/main/resources/i18n/Messages_pt_BR.properties @@ -232,6 +232,7 @@ preferences.res_skip_file=Pular arquivos excedidos #preferences.plugins.install=Install plugin #preferences.plugins.install_btn=Install +#preferences.plugins.uninstall_btn=Uninstall #preferences.plugins.location_id_label=Location id: #preferences.plugins.plugin_jar=Select Plugin jar #preferences.plugins.plugin_jar_label=or @@ -240,6 +241,7 @@ preferences.res_skip_file=Pular arquivos excedidos #preferences.plugins.task.installing=Installing plugin #preferences.plugins.task.uninstalling=Uninstalling plugin #preferences.plugins.task.updating=Updating plugins +#preferences.plugins.task.downloading_list=Downloading plugins list #preferences.cache=Cache #preferences.cache.location=Cache location diff --git a/jadx-gui/src/main/resources/i18n/Messages_ru_RU.properties b/jadx-gui/src/main/resources/i18n/Messages_ru_RU.properties index 0d65a875b..bd7229e1d 100644 --- a/jadx-gui/src/main/resources/i18n/Messages_ru_RU.properties +++ b/jadx-gui/src/main/resources/i18n/Messages_ru_RU.properties @@ -232,6 +232,7 @@ preferences.res_skip_file=Пропускать ресурсы больше че #preferences.plugins.install=Install plugin #preferences.plugins.install_btn=Install +#preferences.plugins.uninstall_btn=Uninstall #preferences.plugins.location_id_label=Location id: #preferences.plugins.plugin_jar=Select Plugin jar #preferences.plugins.plugin_jar_label=or @@ -240,6 +241,7 @@ preferences.res_skip_file=Пропускать ресурсы больше че #preferences.plugins.task.installing=Installing plugin #preferences.plugins.task.uninstalling=Uninstalling plugin #preferences.plugins.task.updating=Updating plugins +#preferences.plugins.task.downloading_list=Downloading plugins list #preferences.cache=Cache #preferences.cache.location=Cache location 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 ccbf1c6ca..9395ad26c 100644 --- a/jadx-gui/src/main/resources/i18n/Messages_zh_CN.properties +++ b/jadx-gui/src/main/resources/i18n/Messages_zh_CN.properties @@ -232,6 +232,7 @@ preferences.res_skip_file=跳过文件大小(MB) preferences.plugins.install=安装插件 preferences.plugins.install_btn=安装 +#preferences.plugins.uninstall_btn=Uninstall preferences.plugins.location_id_label=位置ID: preferences.plugins.plugin_jar=选择插件 jar preferences.plugins.plugin_jar_label=或 @@ -240,6 +241,7 @@ preferences.plugins.details=插件详情 preferences.plugins.task.installing=安装插件中 preferences.plugins.task.uninstalling=卸载插件中 preferences.plugins.task.updating=更新插件中 +#preferences.plugins.task.downloading_list=Downloading plugins list preferences.cache=缓存 preferences.cache.location=缓存位置 diff --git a/jadx-gui/src/main/resources/i18n/Messages_zh_TW.properties b/jadx-gui/src/main/resources/i18n/Messages_zh_TW.properties index 6265e07b2..26d7d9ae7 100644 --- a/jadx-gui/src/main/resources/i18n/Messages_zh_TW.properties +++ b/jadx-gui/src/main/resources/i18n/Messages_zh_TW.properties @@ -232,6 +232,7 @@ preferences.res_skip_file=略過大於此值的檔案 (MB) preferences.plugins.install=安裝外掛程式 preferences.plugins.install_btn=安裝 +#preferences.plugins.uninstall_btn=Uninstall preferences.plugins.location_id_label=位置 id: preferences.plugins.plugin_jar=選擇外掛程式 jar preferences.plugins.plugin_jar_label=或 @@ -240,6 +241,7 @@ preferences.plugins.update_all=全部更新 preferences.plugins.task.installing=正在安裝外掛程式 preferences.plugins.task.uninstalling=正在解除安裝外掛程式 preferences.plugins.task.updating=正在更新外掛程式 +#preferences.plugins.task.downloading_list=Downloading plugins list #preferences.cache=Cache #preferences.cache.location=Cache location diff --git a/jadx-plugins-tools/src/main/java/jadx/plugins/tools/JadxPluginsList.java b/jadx-plugins-tools/src/main/java/jadx/plugins/tools/JadxPluginsList.java new file mode 100644 index 000000000..966a2376c --- /dev/null +++ b/jadx-plugins-tools/src/main/java/jadx/plugins/tools/JadxPluginsList.java @@ -0,0 +1,86 @@ +package jadx.plugins.tools; + +import java.io.InputStreamReader; +import java.io.Reader; +import java.lang.reflect.Type; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; + +import org.jetbrains.annotations.Nullable; +import org.jetbrains.annotations.TestOnly; + +import com.google.gson.Gson; +import com.google.gson.reflect.TypeToken; + +import jadx.api.plugins.utils.ZipSecurity; +import jadx.core.utils.files.FileUtils; +import jadx.plugins.tools.data.JadxPluginMetadata; +import jadx.plugins.tools.resolvers.github.GithubTools; +import jadx.plugins.tools.resolvers.github.LocationInfo; +import jadx.plugins.tools.resolvers.github.data.Asset; +import jadx.plugins.tools.resolvers.github.data.Release; +import jadx.plugins.tools.utils.PluginUtils; + +/** + * TODO: implement list caching (on disk) with check for new release + */ +public class JadxPluginsList { + private static final JadxPluginsList INSTANCE = new JadxPluginsList(); + + private static final Type LIST_TYPE = new TypeToken>() { + }.getType(); + + public static JadxPluginsList getInstance() { + return INSTANCE; + } + + private @Nullable List cache; + + private JadxPluginsList() { + } + + public synchronized List fetch() { + if (cache != null) { + return cache; + } + LocationInfo latest = new LocationInfo("jadx-decompiler", "jadx-plugins-list", "list", null); + Release release = GithubTools.fetchRelease(latest); + List assets = release.getAssets(); + if (assets.isEmpty()) { + throw new RuntimeException("Release don't have assets"); + } + Asset listAsset = assets.get(0); + Path tmpListFile = FileUtils.createTempFile("list.zip"); + PluginUtils.downloadFile(listAsset.getDownloadUrl(), tmpListFile); + + List entries = loadListBundle(tmpListFile); + cache = entries; + return entries; + } + + private static List loadListBundle(Path tmpListFile) { + Gson gson = new Gson(); + List entries = new ArrayList<>(); + ZipSecurity.readZipEntries(tmpListFile.toFile(), (entry, in) -> { + if (entry.getName().endsWith(".json")) { + try (Reader reader = new InputStreamReader(in)) { + entries.addAll(gson.fromJson(reader, LIST_TYPE)); + } catch (Exception e) { + throw new RuntimeException("Failed to read plugins list entry: " + entry.getName()); + } + } + }); + return entries; + } + + @TestOnly + public synchronized List fetchFromLocalBundle(Path bundleFile) { + if (cache != null) { + return cache; + } + List entries = loadListBundle(bundleFile); + cache = entries; + return entries; + } +} diff --git a/jadx-plugins-tools/src/main/java/jadx/plugins/tools/JadxPluginsTools.java b/jadx-plugins-tools/src/main/java/jadx/plugins/tools/JadxPluginsTools.java index c2b56b90b..0766b80f4 100644 --- a/jadx-plugins-tools/src/main/java/jadx/plugins/tools/JadxPluginsTools.java +++ b/jadx-plugins-tools/src/main/java/jadx/plugins/tools/JadxPluginsTools.java @@ -1,10 +1,8 @@ package jadx.plugins.tools; import java.io.IOException; -import java.io.InputStream; import java.io.Reader; import java.io.Writer; -import java.net.URI; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; @@ -12,6 +10,7 @@ import java.nio.file.Paths; import java.nio.file.StandardCopyOption; import java.util.ArrayList; import java.util.List; +import java.util.Objects; import java.util.Optional; import java.util.stream.Stream; @@ -20,8 +19,6 @@ import org.jetbrains.annotations.Nullable; import com.google.gson.Gson; import com.google.gson.GsonBuilder; -import dev.dirs.ProjectDirectories; - import jadx.api.plugins.JadxPlugin; import jadx.api.plugins.JadxPluginInfo; import jadx.core.utils.files.FileUtils; @@ -30,8 +27,11 @@ import jadx.plugins.tools.data.JadxPluginMetadata; import jadx.plugins.tools.data.JadxPluginUpdate; import jadx.plugins.tools.resolvers.IJadxPluginResolver; import jadx.plugins.tools.resolvers.ResolversRegistry; +import jadx.plugins.tools.utils.PluginUtils; -import static jadx.core.utils.files.FileUtils.makeDirs; +import static jadx.plugins.tools.utils.PluginFiles.DROPINS_DIR; +import static jadx.plugins.tools.utils.PluginFiles.INSTALLED_DIR; +import static jadx.plugins.tools.utils.PluginFiles.PLUGINS_JSON; public class JadxPluginsTools { private static final JadxPluginsTools INSTANCE = new JadxPluginsTools(); @@ -40,25 +40,19 @@ public class JadxPluginsTools { return INSTANCE; } - private final Path pluginsJson; - private final Path dropins; - private final Path installed; - private JadxPluginsTools() { - ProjectDirectories jadxDirs = ProjectDirectories.from("io.github", "skylot", "jadx"); - Path plugins = Paths.get(jadxDirs.configDir, "plugins"); - makeDirs(plugins); - pluginsJson = plugins.resolve("plugins.json"); - dropins = plugins.resolve("dropins"); - makeDirs(dropins); - installed = plugins.resolve("installed"); - makeDirs(installed); } public JadxPluginMetadata install(String locationId) { + JadxPluginMetadata pluginMetadata = resolveMetadata(locationId); + install(pluginMetadata); + return pluginMetadata; + } + + public JadxPluginMetadata resolveMetadata(String locationId) { JadxPluginMetadata pluginMetadata = ResolversRegistry.resolve(locationId) .orElseThrow(() -> new RuntimeException("Failed to resolve locationId: " + locationId)); - install(pluginMetadata); + fillMetadata(pluginMetadata); return pluginMetadata; } @@ -117,14 +111,6 @@ public class JadxPluginsTools { return true; } - private void deletePluginJar(JadxPluginMetadata plugin) { - try { - Files.deleteIfExists(installed.resolve(plugin.getJar())); - } catch (IOException e) { - // ignore - } - } - public List getInstalled() { return loadPluginsJson().getInstalled(); } @@ -132,9 +118,9 @@ public class JadxPluginsTools { public List getAllPluginJars() { List list = new ArrayList<>(); for (JadxPluginMetadata pluginMetadata : loadPluginsJson().getInstalled()) { - list.add(installed.resolve(pluginMetadata.getJar())); + list.add(INSTALLED_DIR.resolve(pluginMetadata.getJar())); } - collectFromDir(list, dropins); + collectFromDir(list, DROPINS_DIR); return list; } @@ -148,28 +134,20 @@ public class JadxPluginsTools { return null; } JadxPluginMetadata update = updateOpt.get(); - if (update.getVersion().equals(plugin.getVersion())) { + if (Objects.equals(update.getVersion(), plugin.getVersion())) { return null; } + fillMetadata(update); install(update); return update; } - private void install(JadxPluginMetadata metadata) { - Path tmpJar; - if (needDownload(metadata.getJar())) { - tmpJar = FileUtils.createTempFile("plugin.jar"); - downloadJar(metadata.getJar(), tmpJar); - } else { - tmpJar = Paths.get(metadata.getJar()); - } - fillPluginInfoFromJar(metadata, tmpJar); - + public void install(JadxPluginMetadata metadata) { String version = metadata.getVersion(); String fileName = metadata.getPluginId() + (version != null ? '-' + version : "") + ".jar"; - Path pluginJar = installed.resolve(fileName); - copyJar(tmpJar, pluginJar); - metadata.setJar(installed.relativize(pluginJar).toString()); + Path pluginJar = INSTALLED_DIR.resolve(fileName); + copyJar(Paths.get(metadata.getJar()), pluginJar); + metadata.setJar(INSTALLED_DIR.relativize(pluginJar).toString()); JadxInstalledPlugins plugins = loadPluginsJson(); // remove previous version jar @@ -182,28 +160,33 @@ public class JadxPluginsTools { savePluginsJson(plugins); } - private void fillPluginInfoFromJar(JadxPluginMetadata metadata, Path jar) { + private void fillMetadata(JadxPluginMetadata metadata) { + Path tmpJar; + if (needDownload(metadata.getJar())) { + tmpJar = FileUtils.createTempFile("plugin.jar"); + PluginUtils.downloadFile(metadata.getJar(), tmpJar); + metadata.setJar(tmpJar.toAbsolutePath().toString()); + } else { + tmpJar = Paths.get(metadata.getJar()); + } + fillMetadataFromJar(metadata, tmpJar); + } + + private void fillMetadataFromJar(JadxPluginMetadata metadata, Path jar) { try (JadxExternalPluginsLoader loader = new JadxExternalPluginsLoader()) { JadxPlugin jadxPlugin = loader.loadFromJar(jar); JadxPluginInfo pluginInfo = jadxPlugin.getPluginInfo(); metadata.setPluginId(pluginInfo.getPluginId()); metadata.setName(pluginInfo.getName()); metadata.setDescription(pluginInfo.getDescription()); + metadata.setHomepage(pluginInfo.getHomepage()); } } - private boolean needDownload(String jar) { + private static boolean needDownload(String jar) { return jar.startsWith("https://") || jar.startsWith("http://"); } - private void downloadJar(String sourceJar, Path destPath) { - try (InputStream in = URI.create(sourceJar).toURL().openStream()) { - Files.copy(in, destPath, StandardCopyOption.REPLACE_EXISTING); - } catch (Exception e) { - throw new RuntimeException("Failed to download jar: " + sourceJar, e); - } - } - private void copyJar(Path sourceJar, Path destJar) { try { Files.copy(sourceJar, destJar, StandardCopyOption.REPLACE_EXISTING); @@ -212,35 +195,45 @@ public class JadxPluginsTools { } } + private void deletePluginJar(JadxPluginMetadata plugin) { + try { + Files.deleteIfExists(INSTALLED_DIR.resolve(plugin.getJar())); + } catch (IOException e) { + // ignore + } + } + private static Gson buildGson() { - return new GsonBuilder().setPrettyPrinting().create(); + return new GsonBuilder() + .setPrettyPrinting() + .create(); } private JadxInstalledPlugins loadPluginsJson() { - if (!Files.isRegularFile(pluginsJson)) { + if (!Files.isRegularFile(PLUGINS_JSON)) { return new JadxInstalledPlugins(); } - try (Reader reader = Files.newBufferedReader(pluginsJson, StandardCharsets.UTF_8)) { + try (Reader reader = Files.newBufferedReader(PLUGINS_JSON, StandardCharsets.UTF_8)) { return buildGson().fromJson(reader, JadxInstalledPlugins.class); } catch (Exception e) { - throw new RuntimeException("Failed to read file: " + pluginsJson); + throw new RuntimeException("Failed to read file: " + PLUGINS_JSON); } } private void savePluginsJson(JadxInstalledPlugins data) { if (data.getInstalled().isEmpty()) { try { - Files.deleteIfExists(pluginsJson); + Files.deleteIfExists(PLUGINS_JSON); } catch (Exception e) { - throw new RuntimeException("Failed to remove file: " + pluginsJson, e); + throw new RuntimeException("Failed to remove file: " + PLUGINS_JSON, e); } return; } data.getInstalled().sort(null); - try (Writer writer = Files.newBufferedWriter(pluginsJson, StandardCharsets.UTF_8)) { + try (Writer writer = Files.newBufferedWriter(PLUGINS_JSON, StandardCharsets.UTF_8)) { buildGson().toJson(data, writer); } catch (Exception e) { - throw new RuntimeException("Error saving file: " + pluginsJson, e); + throw new RuntimeException("Error saving file: " + PLUGINS_JSON, e); } } diff --git a/jadx-plugins-tools/src/main/java/jadx/plugins/tools/data/JadxPluginMetadata.java b/jadx-plugins-tools/src/main/java/jadx/plugins/tools/data/JadxPluginMetadata.java index 8b2919505..955cd3ae4 100644 --- a/jadx-plugins-tools/src/main/java/jadx/plugins/tools/data/JadxPluginMetadata.java +++ b/jadx-plugins-tools/src/main/java/jadx/plugins/tools/data/JadxPluginMetadata.java @@ -7,6 +7,7 @@ public class JadxPluginMetadata implements Comparable { private String pluginId; private String name; private String description; + private String homepage; private @Nullable String version; private String locationId; private String resolverId; @@ -44,6 +45,14 @@ public class JadxPluginMetadata implements Comparable { this.description = description; } + public String getHomepage() { + return homepage; + } + + public void setHomepage(String homepage) { + this.homepage = homepage; + } + public String getLocationId() { return locationId; } diff --git a/jadx-plugins-tools/src/main/java/jadx/plugins/tools/resolvers/file/LocalFileResolver.java b/jadx-plugins-tools/src/main/java/jadx/plugins/tools/resolvers/file/LocalFileResolver.java index cc7c7ac04..51807a364 100644 --- a/jadx-plugins-tools/src/main/java/jadx/plugins/tools/resolvers/file/LocalFileResolver.java +++ b/jadx-plugins-tools/src/main/java/jadx/plugins/tools/resolvers/file/LocalFileResolver.java @@ -6,7 +6,7 @@ import java.util.Optional; import jadx.plugins.tools.data.JadxPluginMetadata; import jadx.plugins.tools.resolvers.IJadxPluginResolver; -import static jadx.plugins.tools.utils.PluginsUtils.removePrefix; +import static jadx.plugins.tools.utils.PluginUtils.removePrefix; public class LocalFileResolver implements IJadxPluginResolver { @Override diff --git a/jadx-plugins-tools/src/main/java/jadx/plugins/tools/resolvers/github/GithubReleaseResolver.java b/jadx-plugins-tools/src/main/java/jadx/plugins/tools/resolvers/github/GithubReleaseResolver.java index eb499a7ef..b45f128fb 100644 --- a/jadx-plugins-tools/src/main/java/jadx/plugins/tools/resolvers/github/GithubReleaseResolver.java +++ b/jadx-plugins-tools/src/main/java/jadx/plugins/tools/resolvers/github/GithubReleaseResolver.java @@ -1,19 +1,8 @@ package jadx.plugins.tools.resolvers.github; -import java.io.IOException; -import java.io.InputStreamReader; -import java.io.Reader; -import java.lang.reflect.Type; -import java.net.HttpURLConnection; -import java.net.URI; -import java.nio.charset.StandardCharsets; import java.util.List; import java.util.Optional; import java.util.regex.Pattern; -import java.util.stream.Collectors; - -import com.google.gson.Gson; -import com.google.gson.reflect.TypeToken; import jadx.core.utils.ListUtils; import jadx.plugins.tools.data.JadxPluginMetadata; @@ -21,24 +10,18 @@ import jadx.plugins.tools.resolvers.IJadxPluginResolver; import jadx.plugins.tools.resolvers.github.data.Asset; import jadx.plugins.tools.resolvers.github.data.Release; -import static jadx.plugins.tools.utils.PluginsUtils.removePrefix; +import static jadx.plugins.tools.utils.PluginUtils.removePrefix; public class GithubReleaseResolver implements IJadxPluginResolver { - private static final String GITHUB_API_URL = "https://api.github.com/"; private static final Pattern VERSION_PATTERN = Pattern.compile("v?\\d+\\.\\d+(\\.\\d+)?"); - private static final Type RELEASE_TYPE = new TypeToken() { - }.getType(); - private static final Type RELEASE_LIST_TYPE = new TypeToken>() { - }.getType(); - @Override public Optional resolve(String locationId) { LocationInfo info = parseLocation(locationId); if (info == null) { return Optional.empty(); } - Release release = fetchRelease(info); + Release release = GithubTools.fetchRelease(info); List assets = release.getAssets(); String releaseVersion = removePrefix(release.getName(), "v"); Asset asset = searchPluginAsset(assets, info.getArtifactPrefix(), releaseVersion); @@ -83,7 +66,7 @@ public class GithubReleaseResolver implements IJadxPluginResolver { if (exactAsset != null) { return exactAsset; } - // search without version + // search without version filter Asset foundAsset = ListUtils.filterOnlyOne(assets, a -> { String assetFileName = a.getName(); return assetFileName.startsWith(artifactPrefix) && assetFileName.endsWith(".jar"); @@ -102,42 +85,6 @@ public class GithubReleaseResolver implements IJadxPluginResolver { return baseLocation + ':' + info.getArtifactPrefix(); } - private static Release fetchRelease(LocationInfo info) { - String projectUrl = GITHUB_API_URL + "repos/" + info.getOwner() + "/" + info.getProject(); - String version = info.getVersion(); - if (version == null) { - // get latest version - return get(projectUrl + "/releases/latest", RELEASE_TYPE); - } - // search version among all releases (by name) - List releases = get(projectUrl + "/releases", RELEASE_LIST_TYPE); - return releases.stream() - .filter(r -> r.getName().equals(version)) - .findFirst() - .orElseThrow(() -> new RuntimeException("Release with version: " + version + " not found." - + " Available versions: " + releases.stream().map(Release::getName).collect(Collectors.joining(", ")))); - } - - private static T get(String url, Type type) { - HttpURLConnection con; - try { - con = (HttpURLConnection) URI.create(url).toURL().openConnection(); - con.setRequestMethod("GET"); - int code = con.getResponseCode(); - if (code != 200) { - // TODO: support redirects? - throw new RuntimeException("Request failed, response: " + code + ", url: " + url); - } - } catch (IOException e) { - throw new RuntimeException("Request failed, url: " + url, e); - } - try (Reader reader = new InputStreamReader(con.getInputStream(), StandardCharsets.UTF_8)) { - return new Gson().fromJson(reader, type); - } catch (Exception e) { - throw new RuntimeException("Failed to parse response, url: " + url, e); - } - } - @Override public String id() { return "github-release"; diff --git a/jadx-plugins-tools/src/main/java/jadx/plugins/tools/resolvers/github/GithubTools.java b/jadx-plugins-tools/src/main/java/jadx/plugins/tools/resolvers/github/GithubTools.java new file mode 100644 index 000000000..235b091b0 --- /dev/null +++ b/jadx-plugins-tools/src/main/java/jadx/plugins/tools/resolvers/github/GithubTools.java @@ -0,0 +1,61 @@ +package jadx.plugins.tools.resolvers.github; + +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.Reader; +import java.lang.reflect.Type; +import java.net.HttpURLConnection; +import java.net.URI; +import java.nio.charset.StandardCharsets; +import java.util.List; +import java.util.stream.Collectors; + +import com.google.gson.Gson; +import com.google.gson.reflect.TypeToken; + +import jadx.plugins.tools.resolvers.github.data.Release; + +public class GithubTools { + private static final String GITHUB_API_URL = "https://api.github.com/"; + + private static final Type RELEASE_TYPE = new TypeToken() { + }.getType(); + private static final Type RELEASE_LIST_TYPE = new TypeToken>() { + }.getType(); + + public static Release fetchRelease(LocationInfo info) { + String projectUrl = GITHUB_API_URL + "repos/" + info.getOwner() + "/" + info.getProject(); + String version = info.getVersion(); + if (version == null) { + // get latest version + return get(projectUrl + "/releases/latest", RELEASE_TYPE); + } + // search version among all releases (by name) + List releases = get(projectUrl + "/releases", RELEASE_LIST_TYPE); + return releases.stream() + .filter(r -> r.getName().equals(version)) + .findFirst() + .orElseThrow(() -> new RuntimeException("Release with version: " + version + " not found." + + " Available versions: " + releases.stream().map(Release::getName).collect(Collectors.joining(", ")))); + } + + private static T get(String url, Type type) { + HttpURLConnection con; + try { + con = (HttpURLConnection) URI.create(url).toURL().openConnection(); + con.setRequestMethod("GET"); + int code = con.getResponseCode(); + if (code != 200) { + // TODO: support redirects? + throw new RuntimeException("Request failed, response: " + code + ", url: " + url); + } + } catch (IOException e) { + throw new RuntimeException("Request failed, url: " + url, e); + } + try (Reader reader = new InputStreamReader(con.getInputStream(), StandardCharsets.UTF_8)) { + return new Gson().fromJson(reader, type); + } catch (Exception e) { + throw new RuntimeException("Failed to parse response, url: " + url, e); + } + } +} diff --git a/jadx-plugins-tools/src/main/java/jadx/plugins/tools/resolvers/github/LocationInfo.java b/jadx-plugins-tools/src/main/java/jadx/plugins/tools/resolvers/github/LocationInfo.java index 207c879cf..f771865af 100644 --- a/jadx-plugins-tools/src/main/java/jadx/plugins/tools/resolvers/github/LocationInfo.java +++ b/jadx-plugins-tools/src/main/java/jadx/plugins/tools/resolvers/github/LocationInfo.java @@ -2,7 +2,7 @@ package jadx.plugins.tools.resolvers.github; import org.jetbrains.annotations.Nullable; -class LocationInfo { +public class LocationInfo { private final String owner; private final String project; private final String artifactPrefix; diff --git a/jadx-plugins-tools/src/main/java/jadx/plugins/tools/utils/PluginFiles.java b/jadx-plugins-tools/src/main/java/jadx/plugins/tools/utils/PluginFiles.java new file mode 100644 index 000000000..220b5627c --- /dev/null +++ b/jadx-plugins-tools/src/main/java/jadx/plugins/tools/utils/PluginFiles.java @@ -0,0 +1,24 @@ +package jadx.plugins.tools.utils; + +import java.nio.file.Path; +import java.nio.file.Paths; + +import dev.dirs.ProjectDirectories; + +import static jadx.core.utils.files.FileUtils.makeDirs; + +public class PluginFiles { + private static final ProjectDirectories DIRS = ProjectDirectories.from("io.github", "skylot", "jadx"); + + public static final Path PLUGINS_DIR = Paths.get(DIRS.configDir, "plugins"); + public static final Path PLUGINS_JSON = PLUGINS_DIR.resolve("plugins.json"); + public static final Path INSTALLED_DIR = PLUGINS_DIR.resolve("installed"); + public static final Path DROPINS_DIR = PLUGINS_DIR.resolve("dropins"); + + public static final Path CACHE_DIR = Paths.get(DIRS.cacheDir); + + static { + makeDirs(INSTALLED_DIR); + makeDirs(DROPINS_DIR); + } +} diff --git a/jadx-plugins-tools/src/main/java/jadx/plugins/tools/utils/PluginUtils.java b/jadx-plugins-tools/src/main/java/jadx/plugins/tools/utils/PluginUtils.java new file mode 100644 index 000000000..96974f594 --- /dev/null +++ b/jadx-plugins-tools/src/main/java/jadx/plugins/tools/utils/PluginUtils.java @@ -0,0 +1,25 @@ +package jadx.plugins.tools.utils; + +import java.io.InputStream; +import java.net.URI; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; + +public class PluginUtils { + + public static String removePrefix(String str, String prefix) { + if (str.startsWith(prefix)) { + return str.substring(prefix.length()); + } + return str; + } + + public static void downloadFile(String fileUrl, Path destPath) { + try (InputStream in = URI.create(fileUrl).toURL().openStream()) { + Files.copy(in, destPath, StandardCopyOption.REPLACE_EXISTING); + } catch (Exception e) { + throw new RuntimeException("Failed to download file: " + fileUrl, e); + } + } +} diff --git a/jadx-plugins-tools/src/main/java/jadx/plugins/tools/utils/PluginsUtils.java b/jadx-plugins-tools/src/main/java/jadx/plugins/tools/utils/PluginsUtils.java deleted file mode 100644 index ee00825b1..000000000 --- a/jadx-plugins-tools/src/main/java/jadx/plugins/tools/utils/PluginsUtils.java +++ /dev/null @@ -1,11 +0,0 @@ -package jadx.plugins.tools.utils; - -public class PluginsUtils { - - public static String removePrefix(String str, String prefix) { - if (str.startsWith(prefix)) { - return str.substring(prefix.length()); - } - return str; - } -} diff --git a/jadx-plugins/jadx-java-input/src/main/java/jadx/plugins/input/java/JavaInputPlugin.java b/jadx-plugins/jadx-java-input/src/main/java/jadx/plugins/input/java/JavaInputPlugin.java index 973fdeaa2..1b2999aa1 100644 --- a/jadx-plugins/jadx-java-input/src/main/java/jadx/plugins/input/java/JavaInputPlugin.java +++ b/jadx-plugins/jadx-java-input/src/main/java/jadx/plugins/input/java/JavaInputPlugin.java @@ -18,14 +18,9 @@ import jadx.plugins.input.java.utils.JavaClassParseException; public class JavaInputPlugin implements JadxPlugin { - public static final JadxPluginInfo PLUGIN_INFO = new JadxPluginInfo( - "java-input", - "JavaInput", - "Load .class and .jar files"); - @Override public JadxPluginInfo getPluginInfo() { - return PLUGIN_INFO; + return new JadxPluginInfo("java-input", "Java Input", "Load .class and .jar files"); } @Override diff --git a/jadx-plugins/jadx-raung-input/src/main/java/jadx/plugins/input/raung/RaungInputPlugin.java b/jadx-plugins/jadx-raung-input/src/main/java/jadx/plugins/input/raung/RaungInputPlugin.java index 604168e73..76d8709ff 100644 --- a/jadx-plugins/jadx-raung-input/src/main/java/jadx/plugins/input/raung/RaungInputPlugin.java +++ b/jadx-plugins/jadx-raung-input/src/main/java/jadx/plugins/input/raung/RaungInputPlugin.java @@ -15,10 +15,7 @@ public class RaungInputPlugin implements JadxPlugin, JadxCodeInput { @Override public JadxPluginInfo getPluginInfo() { - return new JadxPluginInfo( - "raung-input", - "RaungInput", - "Load .raung files"); + return new JadxPluginInfo("raung-input", "Raung Input", "Load .raung files"); } @Override diff --git a/jadx-plugins/jadx-smali-input/src/main/java/jadx/plugins/input/smali/SmaliInputPlugin.java b/jadx-plugins/jadx-smali-input/src/main/java/jadx/plugins/input/smali/SmaliInputPlugin.java index e1d219107..58e47f493 100644 --- a/jadx-plugins/jadx-smali-input/src/main/java/jadx/plugins/input/smali/SmaliInputPlugin.java +++ b/jadx-plugins/jadx-smali-input/src/main/java/jadx/plugins/input/smali/SmaliInputPlugin.java @@ -17,7 +17,7 @@ public class SmaliInputPlugin implements JadxPlugin, JadxCodeInput { @Override public JadxPluginInfo getPluginInfo() { - return new JadxPluginInfo("smali-input", "SmaliInput", "Load .smali files"); + return new JadxPluginInfo("smali-input", "Smali Input", "Load .smali files"); } @Override