diff --git a/jadx-core/src/main/java/jadx/core/utils/files/FileUtils.java b/jadx-core/src/main/java/jadx/core/utils/files/FileUtils.java index 401e949d..b2ee442b 100644 --- a/jadx-core/src/main/java/jadx/core/utils/files/FileUtils.java +++ b/jadx-core/src/main/java/jadx/core/utils/files/FileUtils.java @@ -236,12 +236,12 @@ public class FileUtils { public static void writeFile(Path file, String data) throws IOException { FileUtils.makeDirsForFile(file); - Files.write(file, data.getBytes(StandardCharsets.UTF_8), + Files.writeString(file, data, StandardCharsets.UTF_8, StandardOpenOption.WRITE, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING); } public static String readFile(Path textFile) throws IOException { - return new String(Files.readAllBytes(textFile), StandardCharsets.UTF_8); + return Files.readString(textFile); } @NotNull diff --git a/jadx-gui/src/main/java/jadx/gui/settings/ui/plugins/PluginSettingsGroup.java b/jadx-gui/src/main/java/jadx/gui/settings/ui/plugins/PluginSettingsGroup.java index 2cc5254d..437a0ed5 100644 --- a/jadx-gui/src/main/java/jadx/gui/settings/ui/plugins/PluginSettingsGroup.java +++ b/jadx-gui/src/main/java/jadx/gui/settings/ui/plugins/PluginSettingsGroup.java @@ -5,6 +5,7 @@ import java.awt.Component; import java.awt.Dimension; import java.awt.Font; import java.util.ArrayList; +import java.util.Collections; import java.util.Comparator; import java.util.HashMap; import java.util.List; @@ -38,6 +39,7 @@ import jadx.core.utils.StringUtils; import jadx.core.utils.Utils; import jadx.gui.ui.MainWindow; import jadx.gui.utils.NLS; +import jadx.gui.utils.UiUtils; import jadx.plugins.tools.JadxPluginsList; import jadx.plugins.tools.JadxPluginsTools; import jadx.plugins.tools.data.JadxPluginMetadata; @@ -89,30 +91,12 @@ class PluginSettingsGroup implements ISettingsGroup { actionsPanel.add(Box.createRigidArea(new Dimension(5, 0))); actionsPanel.add(updateAllBtn); - List installed = JadxPluginsTools.getInstance().getInstalled(); - Map installedMap = new HashMap<>(installed.size()); - installed.forEach(p -> installedMap.put(p.getPluginId(), p)); - - List nodes = new ArrayList<>(installed.size() + 3); - for (PluginContext plugin : installedPlugins) { - nodes.add(new InstalledPluginNode(plugin, installedMap.get(plugin.getPluginId()))); - } - 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); - listModel.addElement(new TitleNode("Available")); - JList pluginList = new JList<>(listModel); pluginList.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); pluginList.setCellRenderer(new PluginsListCellRenderer()); pluginList.addListSelectionListener(ev -> onSelection(pluginList.getSelectedValue())); - loadAvailablePlugins(listModel, installedPlugins); - JScrollPane scrollPane = new JScrollPane(pluginList); scrollPane.setMinimumSize(new Dimension(80, 120)); @@ -132,29 +116,57 @@ class PluginSettingsGroup implements ISettingsGroup { mainPanel.setBorder(BorderFactory.createTitledBorder(title)); mainPanel.add(actionsPanel, BorderLayout.PAGE_START); mainPanel.add(splitPanel, BorderLayout.CENTER); + + applyData(listModel); return mainPanel; } - private void loadAvailablePlugins(DefaultListModel listModel, List installedPlugins) { - List list = new ArrayList<>(); + private void applyData(DefaultListModel listModel) { + List installed = JadxPluginsTools.getInstance().getInstalled(); + Map installedMap = new HashMap<>(installed.size()); + installed.forEach(p -> installedMap.put(p.getPluginId(), p)); + + List nodes = new ArrayList<>(installed.size() + 3); + for (PluginContext plugin : installedPlugins) { + nodes.add(new InstalledPluginNode(plugin, installedMap.get(plugin.getPluginId()))); + } + nodes.sort(Comparator.comparing(BasePluginListNode::getTitle)); + + fillListModel(listModel, nodes, Collections.emptyList()); + loadAvailablePlugins(listModel, nodes, installedPlugins); + } + + private static void fillListModel(DefaultListModel listModel, + List nodes, List available) { + listModel.clear(); + 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); + listModel.addElement(new TitleNode("Available")); + listModel.addAll(available); + } + + private void loadAvailablePlugins(DefaultListModel listModel, + List nodes, List installedPlugins) { mainWindow.getBackgroundExecutor().execute( NLS.str("preferences.plugins.task.downloading_list"), () -> { - List availablePlugins; try { - availablePlugins = JadxPluginsList.getInstance().fetch(); + JadxPluginsList.getInstance().get(availablePlugins -> { + Set installed = installedPlugins.stream() + .map(PluginContext::getPluginId) + .collect(Collectors.toSet()); + List availableNodes = availablePlugins.stream() + .filter(availablePlugin -> !installed.contains(availablePlugin.getPluginId())) + .map(AvailablePluginNode::new) + .collect(Collectors.toList()); + UiUtils.uiRunAndWait(() -> fillListModel(listModel, nodes, availableNodes)); + }); } 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) { 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 index 966a2376..b5743b3b 100644 --- a/jadx-plugins-tools/src/main/java/jadx/plugins/tools/JadxPluginsList.java +++ b/jadx-plugins-tools/src/main/java/jadx/plugins/tools/JadxPluginsList.java @@ -3,18 +3,22 @@ package jadx.plugins.tools; import java.io.InputStreamReader; import java.io.Reader; import java.lang.reflect.Type; +import java.nio.file.Files; import java.nio.file.Path; import java.util.ArrayList; import java.util.List; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Consumer; import org.jetbrains.annotations.Nullable; -import org.jetbrains.annotations.TestOnly; import com.google.gson.Gson; +import com.google.gson.GsonBuilder; import com.google.gson.reflect.TypeToken; import jadx.api.plugins.utils.ZipSecurity; import jadx.core.utils.files.FileUtils; +import jadx.plugins.tools.data.JadxPluginListCache; import jadx.plugins.tools.data.JadxPluginMetadata; import jadx.plugins.tools.resolvers.github.GithubTools; import jadx.plugins.tools.resolvers.github.LocationInfo; @@ -22,6 +26,8 @@ import jadx.plugins.tools.resolvers.github.data.Asset; import jadx.plugins.tools.resolvers.github.data.Release; import jadx.plugins.tools.utils.PluginUtils; +import static jadx.plugins.tools.utils.PluginFiles.PLUGINS_LIST_CACHE; + /** * TODO: implement list caching (on disk) with check for new release */ @@ -31,32 +37,96 @@ public class JadxPluginsList { private static final Type LIST_TYPE = new TypeToken>() { }.getType(); + private static final Type CACHE_TYPE = new TypeToken() { + }.getType(); + public static JadxPluginsList getInstance() { return INSTANCE; } - private @Nullable List cache; + private @Nullable JadxPluginListCache loadedList; private JadxPluginsList() { } - public synchronized List fetch() { - if (cache != null) { - return cache; + /** + * List provider with update callback. + * Can be called one or two times: + *
+ * - Apply cached data first + *
+ * - If update is available, apply data after fetch + *
+ * Method call is blocking. + */ + public synchronized void get(Consumer> consumer) { + if (loadedList != null) { + consumer.accept(loadedList.getList()); + return; } + JadxPluginListCache listCache = loadCache(); + if (listCache != null) { + consumer.accept(listCache.getList()); + } + Release release = fetchLatestRelease(); + if (listCache == null || !listCache.getVersion().equals(release.getName())) { + JadxPluginListCache updatedList = fetchBundle(release); + saveCache(updatedList); + consumer.accept(updatedList.getList()); + } + } + + public List get() { + AtomicReference> holder = new AtomicReference<>(); + get(holder::set); + return holder.get(); + } + + private @Nullable JadxPluginListCache loadCache() { + if (!Files.isRegularFile(PLUGINS_LIST_CACHE)) { + return null; + } + try { + String jsonStr = FileUtils.readFile(PLUGINS_LIST_CACHE); + return buildGson().fromJson(jsonStr, CACHE_TYPE); + } catch (Exception e) { + return null; + } + } + + private void saveCache(JadxPluginListCache listCache) { + try { + String jsonStr = buildGson().toJson(listCache, CACHE_TYPE); + FileUtils.writeFile(PLUGINS_LIST_CACHE, jsonStr); + } catch (Exception e) { + throw new RuntimeException("Error saving file: " + PLUGINS_LIST_CACHE, e); + } + loadedList = listCache; + } + + private static Gson buildGson() { + return new GsonBuilder().setPrettyPrinting().create(); + } + + private Release fetchLatestRelease() { 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); + return release; + } + + private JadxPluginListCache fetchBundle(Release release) { + Asset listAsset = release.getAssets().get(0); Path tmpListFile = FileUtils.createTempFile("list.zip"); PluginUtils.downloadFile(listAsset.getDownloadUrl(), tmpListFile); - List entries = loadListBundle(tmpListFile); - cache = entries; - return entries; + JadxPluginListCache listCache = new JadxPluginListCache(); + listCache.setVersion(release.getName()); + listCache.setList(loadListBundle(tmpListFile)); + return listCache; } private static List loadListBundle(Path tmpListFile) { @@ -73,14 +143,4 @@ public class JadxPluginsList { }); 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/data/JadxPluginListCache.java b/jadx-plugins-tools/src/main/java/jadx/plugins/tools/data/JadxPluginListCache.java new file mode 100644 index 00000000..2c24936b --- /dev/null +++ b/jadx-plugins-tools/src/main/java/jadx/plugins/tools/data/JadxPluginListCache.java @@ -0,0 +1,24 @@ +package jadx.plugins.tools.data; + +import java.util.List; + +public class JadxPluginListCache { + private String version; + private List list; + + public String getVersion() { + return version; + } + + public void setVersion(String version) { + this.version = version; + } + + public List getList() { + return list; + } + + public void setList(List list) { + this.list = list; + } +} 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 index 220b5627..693e5235 100644 --- 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 @@ -10,12 +10,13 @@ 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"); + private 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); + private static final Path CACHE_DIR = Paths.get(DIRS.cacheDir); + public static final Path PLUGINS_LIST_CACHE = CACHE_DIR.resolve("plugin-list.json"); static { makeDirs(INSTALLED_DIR);