From e6b919007cb3d0a66f3369000845ed894143c25e Mon Sep 17 00:00:00 2001 From: Skylot Date: Tue, 16 Sep 2014 21:50:44 +0400 Subject: [PATCH] gui: add new version notification --- jadx-gui/build.gradle | 1 + .../main/java/jadx/gui/ui/AboutDialog.java | 8 +- .../src/main/java/jadx/gui/ui/MainWindow.java | 30 ++++- .../main/java/jadx/gui/update/JadxUpdate.java | 105 ++++++++++++++++++ .../jadx/gui/update/VersionComparator.java | 56 ++++++++++ .../main/java/jadx/gui/update/data/Asset.java | 58 ++++++++++ .../java/jadx/gui/update/data/Release.java | 56 ++++++++++ .../src/main/java/jadx/gui/utils/Link.java | 90 +++++++++++++++ .../resources/i18n/Messages_en_US.properties | 2 + .../gui/tests/TestVersionsComparator.groovy | 31 ++++++ 10 files changed, 428 insertions(+), 9 deletions(-) create mode 100644 jadx-gui/src/main/java/jadx/gui/update/JadxUpdate.java create mode 100644 jadx-gui/src/main/java/jadx/gui/update/VersionComparator.java create mode 100644 jadx-gui/src/main/java/jadx/gui/update/data/Asset.java create mode 100644 jadx-gui/src/main/java/jadx/gui/update/data/Release.java create mode 100644 jadx-gui/src/main/java/jadx/gui/utils/Link.java create mode 100644 jadx-gui/src/test/groovy/jadx/gui/tests/TestVersionsComparator.groovy diff --git a/jadx-gui/build.gradle b/jadx-gui/build.gradle index 3ae93258f..a9b46e449 100644 --- a/jadx-gui/build.gradle +++ b/jadx-gui/build.gradle @@ -6,6 +6,7 @@ dependencies { compile(project(":jadx-core")) compile(project(":jadx-cli")) compile 'com.fifesoft:rsyntaxtextarea:2.5.0' + compile 'com.google.code.gson:gson:2.2.4' } applicationDistribution.with { diff --git a/jadx-gui/src/main/java/jadx/gui/ui/AboutDialog.java b/jadx-gui/src/main/java/jadx/gui/ui/AboutDialog.java index 8c3a62c86..8fc67ee8c 100644 --- a/jadx-gui/src/main/java/jadx/gui/ui/AboutDialog.java +++ b/jadx-gui/src/main/java/jadx/gui/ui/AboutDialog.java @@ -1,8 +1,9 @@ package jadx.gui.ui; -import jadx.core.Jadx; +import jadx.api.JadxDecompiler; import jadx.gui.utils.NLS; +import javax.swing.BorderFactory; import javax.swing.Box; import javax.swing.BoxLayout; import javax.swing.JButton; @@ -26,7 +27,7 @@ class AboutDialog extends JDialog { public final void initUI() { Font font = new Font("Serif", Font.BOLD, 13); - JLabel name = new JLabel("JADX"); + JLabel name = new JLabel("jadx"); name.setFont(font); name.setAlignmentX(0.5f); @@ -34,11 +35,12 @@ class AboutDialog extends JDialog { desc.setFont(font); desc.setAlignmentX(0.5f); - JLabel version = new JLabel("version: " + Jadx.getVersion()); + JLabel version = new JLabel("version: " + JadxDecompiler.getVersion()); version.setFont(font); version.setAlignmentX(0.5f); JPanel textPane = new JPanel(); + textPane.setBorder(BorderFactory.createEmptyBorder(15, 15, 15, 15)); textPane.setLayout(new BoxLayout(textPane, BoxLayout.PAGE_AXIS)); textPane.add(Box.createRigidArea(new Dimension(0, 10))); textPane.add(name); 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 bf20f1427..fc2aabd12 100644 --- a/jadx-gui/src/main/java/jadx/gui/ui/MainWindow.java +++ b/jadx-gui/src/main/java/jadx/gui/ui/MainWindow.java @@ -4,11 +4,15 @@ import jadx.gui.JadxWrapper; import jadx.gui.treemodel.JClass; import jadx.gui.treemodel.JNode; import jadx.gui.treemodel.JRoot; +import jadx.gui.update.JadxUpdate; +import jadx.gui.update.data.Release; import jadx.gui.utils.JadxPreferences; +import jadx.gui.utils.Link; import jadx.gui.utils.NLS; import jadx.gui.utils.Position; import jadx.gui.utils.Utils; +import javax.swing.Box; import javax.swing.ImageIcon; import javax.swing.JButton; import javax.swing.JCheckBoxMenuItem; @@ -83,12 +87,25 @@ public class MainWindow extends JFrame { private JCheckBoxMenuItem flatPkgMenuItem; private JToggleButton flatPkgButton; private boolean isFlattenPackage; + private Link updateLink; public MainWindow(JadxWrapper wrapper) { this.wrapper = wrapper; initUI(); initMenuAndToolbar(); + JadxUpdate.check(new JadxUpdate.IUpdateCallback() { + @Override + public void onUpdate(final Release r) { + SwingUtilities.invokeLater(new Runnable() { + @Override + public void run() { + updateLink.setText(String.format(NLS.str("menu.update_label"), r.getName())); + updateLink.setVisible(true); + } + }); + } + }); } public void openFile() { @@ -96,16 +113,13 @@ public class MainWindow extends JFrame { fileChooser.setAcceptAllFileFilterUsed(true); fileChooser.setFileFilter(new FileNameExtensionFilter("supported files", "dex", "apk", "jar")); fileChooser.setToolTipText(NLS.str("file.open")); - String currentDirectory = JadxPreferences.getLastOpenFilePath(); if (!currentDirectory.isEmpty()) { fileChooser.setCurrentDirectory(new File(currentDirectory)); } - int ret = fileChooser.showDialog(mainPanel, NLS.str("file.open")); if (ret == JFileChooser.APPROVE_OPTION) { JadxPreferences.putLastOpenFilePath(fileChooser.getCurrentDirectory().getPath()); - openFile(fileChooser.getSelectedFile()); } } @@ -125,12 +139,11 @@ public class MainWindow extends JFrame { if (!currentDirectory.isEmpty()) { fileChooser.setCurrentDirectory(new File(currentDirectory)); } - + int ret = fileChooser.showDialog(mainPanel, NLS.str("file.select")); if (ret == JFileChooser.APPROVE_OPTION) { JadxPreferences.putLastSaveFilePath(fileChooser.getCurrentDirectory().getPath()); - - ProgressMonitor progressMonitor = new ProgressMonitor(mainPanel, "Saving sources", "", 0, 100); + ProgressMonitor progressMonitor = new ProgressMonitor(mainPanel, NLS.str("msg.saving_sources"), "", 0, 100); progressMonitor.setMillisToPopup(500); wrapper.saveAll(fileChooser.getSelectedFile(), progressMonitor); } @@ -378,6 +391,11 @@ public class MainWindow extends JFrame { forwardButton.setToolTipText(NLS.str("nav.forward")); toolbar.add(forwardButton); + toolbar.add(Box.createHorizontalGlue()); + updateLink = new Link("", JadxUpdate.JADX_RELEASES_URL); + updateLink.setVisible(false); + toolbar.add(updateLink); + mainPanel.add(toolbar, BorderLayout.NORTH); } diff --git a/jadx-gui/src/main/java/jadx/gui/update/JadxUpdate.java b/jadx-gui/src/main/java/jadx/gui/update/JadxUpdate.java new file mode 100644 index 000000000..8952747c7 --- /dev/null +++ b/jadx-gui/src/main/java/jadx/gui/update/JadxUpdate.java @@ -0,0 +1,105 @@ +package jadx.gui.update; + +import jadx.api.JadxDecompiler; +import jadx.gui.update.data.Release; + +import java.io.InputStreamReader; +import java.io.Reader; +import java.lang.reflect.Type; +import java.net.HttpURLConnection; +import java.net.URL; +import java.util.Collections; +import java.util.Comparator; +import java.util.Iterator; +import java.util.List; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.google.gson.Gson; +import com.google.gson.reflect.TypeToken; + +public class JadxUpdate { + private static final Logger LOG = LoggerFactory.getLogger(JadxUpdate.class); + + public static final String JADX_RELEASES_URL = "https://github.com/skylot/jadx/releases"; + + private static final String GITHUB_API_URL = "https://api.github.com/"; + private static final String GITHUB_RELEASES_URL = GITHUB_API_URL + "repos/skylot/jadx/releases"; + + private static final Gson GSON = new Gson(); + + private static final Type RELEASES_LIST_TYPE = new TypeToken>() {}.getType(); + + private static final Comparator RELEASE_COMPARATOR = new Comparator() { + @Override + public int compare(Release o1, Release o2) { + return VersionComparator.checkAndCompare(o1.getName(), o2.getName()); + } + }; + + public static interface IUpdateCallback { + void onUpdate(Release r); + } + + public static void check(final IUpdateCallback callback) { + Runnable run = new Runnable() { + @Override + public void run() { + try { + Release release = checkForNewRelease(); + if (release != null) { + callback.onUpdate(release); + } + } catch (Exception e) { + LOG.debug("Jadx update error", e); + } + } + }; + Thread thread = new Thread(run); + thread.setName("Jadx update thread"); + thread.setPriority(Thread.MIN_PRIORITY); + thread.start(); + } + + private static Release checkForNewRelease() throws Exception { + String version = JadxDecompiler.getVersion(); + if (version.contains("dev")) { + LOG.debug("Ignore check for update: development version"); + return null; + } + + List list = get(GITHUB_RELEASES_URL, RELEASES_LIST_TYPE); + if (list == null) { + return null; + } + for (Iterator it = list.iterator(); it.hasNext(); ) { + Release release = it.next(); + if (release.getName().equalsIgnoreCase(version) + || release.isPrerelease()) { + it.remove(); + } + } + if (list.isEmpty()) { + return null; + } + Collections.sort(list, RELEASE_COMPARATOR); + Release latest = list.get(list.size() - 1); + if (VersionComparator.checkAndCompare(version, latest.getName()) == 0) { + return null; + } + LOG.debug("Found new version: {}", latest); + return latest; + } + + private static T get(String url, Type type) throws Exception { + URL obj = new URL(url); + HttpURLConnection con = (HttpURLConnection) obj.openConnection(); + con.setRequestMethod("GET"); + if (con.getResponseCode() == 200) { + Reader reader = new InputStreamReader(con.getInputStream()); + return GSON.fromJson(reader, type); + } + return null; + } +} diff --git a/jadx-gui/src/main/java/jadx/gui/update/VersionComparator.java b/jadx-gui/src/main/java/jadx/gui/update/VersionComparator.java new file mode 100644 index 000000000..7a717bd2e --- /dev/null +++ b/jadx-gui/src/main/java/jadx/gui/update/VersionComparator.java @@ -0,0 +1,56 @@ +package jadx.gui.update; + +public class VersionComparator { + + public static int checkAndCompare(String str1, String str2) { + try { + return compare(clean(str1), clean(str2)); + } catch (NumberFormatException e) { + return -2; + } + } + + private static String clean(String str) { + String result = str.trim().toLowerCase(); + if (result.charAt(0) == 'v') { + result = result.substring(1); + } + return result; + } + + public static int compare(String str1, String str2) { + String[] s1 = str1.split("\\."); + int l1 = s1.length; + String[] s2 = str2.split("\\."); + int l2 = s2.length; + + int i = 0; + // skip equals parts + while (i < l1 && i < l2) { + if (!s1[i].equals(s2[i])) { + break; + } + i++; + } + // compare first non-equal ordinal number + if (i < l1 && i < l2) { + return Integer.valueOf(s1[i]).compareTo(Integer.valueOf(s2[i])); + } + boolean checkFirst = l1 > l2; + boolean zeroTail = isZeroTail(checkFirst ? s1 : s2, i); + if (zeroTail) { + return 0; + } + return checkFirst ? 1 : -1; + } + + private static boolean isZeroTail(String[] arr, int pos) { + for (int i = pos; i < arr.length; i++) { + String s = arr[i]; + if (Integer.valueOf(s) != 0) { + return false; + } + } + return true; + } +} diff --git a/jadx-gui/src/main/java/jadx/gui/update/data/Asset.java b/jadx-gui/src/main/java/jadx/gui/update/data/Asset.java new file mode 100644 index 000000000..21648fee6 --- /dev/null +++ b/jadx-gui/src/main/java/jadx/gui/update/data/Asset.java @@ -0,0 +1,58 @@ +package jadx.gui.update.data; + +public class Asset { + private int id; + private String url; + private String name; + private String label; + private long size; + private int download_count; + + public int getId() { + return id; + } + + public void setId(int id) { + this.id = id; + } + + public String getUrl() { + return url; + } + + public void setUrl(String url) { + this.url = url; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getLabel() { + return label; + } + + public void setLabel(String label) { + this.label = label; + } + + public long getSize() { + return size; + } + + public void setSize(long size) { + this.size = size; + } + + public int getDownload_count() { + return download_count; + } + + public void setDownload_count(int download_count) { + this.download_count = download_count; + } +} diff --git a/jadx-gui/src/main/java/jadx/gui/update/data/Release.java b/jadx-gui/src/main/java/jadx/gui/update/data/Release.java new file mode 100644 index 000000000..48fc55373 --- /dev/null +++ b/jadx-gui/src/main/java/jadx/gui/update/data/Release.java @@ -0,0 +1,56 @@ +package jadx.gui.update.data; + +import java.util.List; + +public class Release { + private int id; + private String name; + private boolean prerelease; + private List assets; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public int getId() { + return id; + } + + public void setId(int id) { + this.id = id; + } + + public boolean isPrerelease() { + return prerelease; + } + + public void setPrerelease(boolean prerelease) { + this.prerelease = prerelease; + } + + public List getAssets() { + return assets; + } + + public void setAssets(List assets) { + this.assets = assets; + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append(name); + for (Asset asset : getAssets()) { + sb.append('\n'); + sb.append(" ").append(asset.getName()) + .append(", asset id: ").append(asset.getId()) + .append(", size: ").append(asset.getSize()) + .append(", dc: ").append(asset.getDownload_count()); + } + return sb.toString(); + } +} diff --git a/jadx-gui/src/main/java/jadx/gui/utils/Link.java b/jadx-gui/src/main/java/jadx/gui/utils/Link.java new file mode 100644 index 000000000..a7f83653a --- /dev/null +++ b/jadx-gui/src/main/java/jadx/gui/utils/Link.java @@ -0,0 +1,90 @@ +package jadx.gui.utils; + +import javax.swing.JLabel; +import javax.swing.JOptionPane; +import javax.swing.JTextArea; +import java.awt.Color; +import java.awt.Cursor; +import java.awt.Desktop; +import java.awt.event.MouseEvent; +import java.awt.event.MouseListener; +import java.io.IOException; +import java.net.URISyntaxException; +import java.util.Map; + +import static java.awt.Desktop.Action; + +public class Link extends JLabel implements MouseListener { + private String url; + + public Link(String text, String url) { + super(text); + this.url = url; + this.setToolTipText("Open " + url + " in your browser"); + this.addMouseListener(this); + this.setForeground(Color.BLUE); + } + + @Override + public void mouseClicked(MouseEvent arg0) { + browse(); + } + + @Override + public void mouseEntered(MouseEvent arg0) { + setCursor(new Cursor(Cursor.HAND_CURSOR)); + } + + @Override + public void mouseExited(MouseEvent arg0) { + setCursor(new Cursor(Cursor.DEFAULT_CURSOR)); + } + + @Override + public void mousePressed(MouseEvent arg0) { + } + + @Override + public void mouseReleased(MouseEvent arg0) { + } + + private void browse() { + if (Desktop.isDesktopSupported()) { + Desktop desktop = Desktop.getDesktop(); + if (desktop.isSupported(Action.BROWSE)) { + try { + desktop.browse(new java.net.URI(url)); + return; + } catch (IOException e) { + e.printStackTrace(); + } catch (URISyntaxException e) { + e.printStackTrace(); + } + } + } + try { + String os = System.getProperty("os.name").toLowerCase(); + if (os.contains("win")) { + Runtime.getRuntime().exec("rundll32 url.dll,FileProtocolHandler " + url); + return; + } + if (os.contains("mac")) { + Runtime.getRuntime().exec("open " + url); + return; + } + Map env = System.getenv(); + if (env.get("BROWSER") != null) { + Runtime.getRuntime().exec(env.get("BROWSER") + " " + url); + return; + } + } catch (Exception e) { + e.printStackTrace(); + } + showUrlDialog(); + } + + private void showUrlDialog() { + JTextArea urlArea = new JTextArea("Can't open browser. Please browse to:\n"+url); + JOptionPane.showMessageDialog(null, urlArea); + } +} 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 da8332a88..c1b73f9c3 100644 --- a/jadx-gui/src/main/resources/i18n/Messages_en_US.properties +++ b/jadx-gui/src/main/resources/i18n/Messages_en_US.properties @@ -7,6 +7,7 @@ menu.search=Search ... menu.find_in_file=Find in ... menu.help=Help menu.about=About +menu.update_label=New version %s available! file.open=Open file file.save=Save file @@ -43,3 +44,4 @@ search_dialog.field=Field search_dialog.code=Code msg.open_file=Please open file +msg.saving_sources=Saving sources diff --git a/jadx-gui/src/test/groovy/jadx/gui/tests/TestVersionsComparator.groovy b/jadx-gui/src/test/groovy/jadx/gui/tests/TestVersionsComparator.groovy new file mode 100644 index 000000000..93e09c2c9 --- /dev/null +++ b/jadx-gui/src/test/groovy/jadx/gui/tests/TestVersionsComparator.groovy @@ -0,0 +1,31 @@ +package jadx.gui.tests + +import jadx.gui.update.VersionComparator +import spock.lang.Specification + +class TestVersionsComparator extends Specification { + + def "test"() { + expect: + VersionComparator.compare(s1, s2) == expected + VersionComparator.compare(s2, s1) == -expected + + where: + s1 | s2 | expected + "" | "" | 0 + "1" | "1" | 0 + "1" | "2" | -1 + "1.1" | "1.1" | 0 + "0.5" | "0.5" | 0 + "0.5" | "0.5.0" | 0 + "0.5" | "0.5.00" | 0 + "0.5" | "0.5.0.0" | 0 + "0.5" | "0.5.0.1" | -1 + "0.5.0" | "0.5.0" | 0 + "0.5.0" | "0.5.1" | -1 + "0.5" | "0.5.1" | -1 + "0.4.8" | "0.5" | -1 + "0.4.8" | "0.5.0" | -1 + "0.4.8" | "0.6" | -1 + } +}