diff --git a/jadx-gui/src/main/java/jadx/gui/settings/JadxSettings.java b/jadx-gui/src/main/java/jadx/gui/settings/JadxSettings.java index 697adb6bb..2df0674a4 100644 --- a/jadx-gui/src/main/java/jadx/gui/settings/JadxSettings.java +++ b/jadx-gui/src/main/java/jadx/gui/settings/JadxSettings.java @@ -53,7 +53,7 @@ public class JadxSettings extends JadxCLIArgs { private static final Path USER_HOME = Paths.get(System.getProperty("user.home")); private static final int RECENT_PROJECTS_COUNT = 30; - private static final int CURRENT_SETTINGS_VERSION = 20; + private static final int CURRENT_SETTINGS_VERSION = 21; private static final Font DEFAULT_FONT = new RSyntaxTextArea().getFont(); @@ -127,6 +127,7 @@ public class JadxSettings extends JadxCLIArgs { private boolean jumpOnDoubleClick = true; private XposedCodegenLanguage xposedCodegenLanguage = XposedCodegenLanguage.JAVA; + private JadxUpdateChannel jadxUpdateChannel = JadxUpdateChannel.STABLE; /** * UI setting: the width of the tree showing the classes, resources, ... @@ -752,6 +753,14 @@ public class JadxSettings extends JadxCLIArgs { this.xposedCodegenLanguage = language; } + public JadxUpdateChannel getJadxUpdateChannel() { + return jadxUpdateChannel; + } + + public void setJadxUpdateChannel(JadxUpdateChannel channel) { + this.jadxUpdateChannel = channel; + } + public void setTabDndGhostType(TabDndGhostType tabDndGhostType) { this.tabDndGhostType = tabDndGhostType; } @@ -805,6 +814,10 @@ public class JadxSettings extends JadxCLIArgs { tabDndGhostType = TabDndGhostType.OUTLINE; fromVersion++; } + if (fromVersion == 20) { + jadxUpdateChannel = JadxUpdateChannel.STABLE; + fromVersion++; + } if (fromVersion != CURRENT_SETTINGS_VERSION) { LOG.warn("Incorrect settings upgrade. Expected version: {}, got: {}", CURRENT_SETTINGS_VERSION, fromVersion); } diff --git a/jadx-gui/src/main/java/jadx/gui/settings/JadxUpdateChannel.kt b/jadx-gui/src/main/java/jadx/gui/settings/JadxUpdateChannel.kt new file mode 100644 index 000000000..3067b3eaa --- /dev/null +++ b/jadx-gui/src/main/java/jadx/gui/settings/JadxUpdateChannel.kt @@ -0,0 +1,6 @@ +package jadx.gui.settings + +enum class JadxUpdateChannel { + STABLE, + UNSTABLE, +} 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 68c1bb4af..e7eae6e45 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 @@ -58,6 +58,7 @@ import jadx.api.plugins.events.JadxEvents; import jadx.api.plugins.gui.ISettingsGroup; import jadx.gui.settings.JadxSettings; import jadx.gui.settings.JadxSettingsAdapter; +import jadx.gui.settings.JadxUpdateChannel; import jadx.gui.settings.LineNumbersMode; import jadx.gui.settings.XposedCodegenLanguage; import jadx.gui.settings.ui.cache.CacheSettingsGroup; @@ -644,6 +645,14 @@ public class JadxSettingsWindow extends JDialog { mainWindow.loadSettings(); }); + JComboBox updateChannel = + new JComboBox<>(JadxUpdateChannel.getEntries().toArray(new JadxUpdateChannel[0])); + updateChannel.setSelectedItem(settings.getJadxUpdateChannel()); + updateChannel.addActionListener(e -> { + settings.setJadxUpdateChannel((JadxUpdateChannel) updateChannel.getSelectedItem()); + mainWindow.loadSettings(); + }); + SettingsGroup group = new SettingsGroup(NLS.str("preferences.other")); group.addRow(NLS.str("preferences.lineNumbersMode"), lineNumbersMode); group.addRow(NLS.str("preferences.jumpOnDoubleClick"), jumpOnDoubleClick); @@ -652,6 +661,7 @@ public class JadxSettingsWindow extends JDialog { group.addRow(NLS.str("preferences.cfg"), cfg); group.addRow(NLS.str("preferences.raw_cfg"), rawCfg); group.addRow(NLS.str("preferences.xposed_codegen_language"), xposedCodegenLanguage); + group.addRow(NLS.str("preferences.update_channel"), updateChannel); return group; } 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 ec078b100..6a7e6f512 100644 --- a/jadx-gui/src/main/java/jadx/gui/ui/MainWindow.java +++ b/jadx-gui/src/main/java/jadx/gui/ui/MainWindow.java @@ -145,8 +145,6 @@ import jadx.gui.ui.tab.dnd.TabDndController; import jadx.gui.ui.treenodes.StartPageNode; import jadx.gui.ui.treenodes.SummaryNode; import jadx.gui.update.JadxUpdate; -import jadx.gui.update.JadxUpdate.IUpdateCallback; -import jadx.gui.update.data.Release; import jadx.gui.utils.CacheObject; import jadx.gui.utils.FontUtils; import jadx.gui.utils.ILoadListener; @@ -311,15 +309,18 @@ public class MainWindow extends JFrame { if (!settings.isCheckForUpdates()) { return; } - JadxUpdate.check(new IUpdateCallback() { - @Override - public void onUpdate(Release r) { - SwingUtilities.invokeLater(() -> { - updateLink.setText(NLS.str("menu.update_label", r.getName())); - updateLink.setVisible(true); - }); + JadxUpdate.check(settings.getJadxUpdateChannel(), release -> SwingUtilities.invokeLater(() -> { + switch (settings.getJadxUpdateChannel()) { + case STABLE: + updateLink.setUrl(JadxUpdate.JADX_RELEASES_URL); + break; + case UNSTABLE: + updateLink.setUrl(JadxUpdate.JADX_ARTIFACTS_URL); + break; } - }); + updateLink.setText(NLS.str("menu.update_label", release.getName())); + updateLink.setVisible(true); + })); } public void openFileDialog() { @@ -1142,7 +1143,7 @@ public class MainWindow extends JFrame { flatPkgButton.addActionListener(flatPkgAction); flatPkgButton.setToolTipText(NLS.str("menu.flatten")); - updateLink = new Link("", JadxUpdate.JADX_RELEASES_URL); + updateLink = new Link(); updateLink.setVisible(false); JToolBar toolbar = new JToolBar(); diff --git a/jadx-gui/src/main/java/jadx/gui/update/JadxUpdate.java b/jadx-gui/src/main/java/jadx/gui/update/JadxUpdate.java deleted file mode 100644 index 2b959faac..000000000 --- a/jadx-gui/src/main/java/jadx/gui/update/JadxUpdate.java +++ /dev/null @@ -1,90 +0,0 @@ -package jadx.gui.update; - -import java.io.IOException; -import java.io.InputStreamReader; -import java.io.Reader; -import java.lang.reflect.Type; -import java.net.HttpURLConnection; -import java.net.URL; -import java.nio.charset.StandardCharsets; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import com.google.gson.Gson; -import com.google.gson.reflect.TypeToken; - -import jadx.api.JadxDecompiler; -import jadx.core.Jadx; -import jadx.gui.update.data.Release; - -@SuppressWarnings("SameParameterValue") -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_LATEST_RELEASE_URL = GITHUB_API_URL + "repos/skylot/jadx/releases/latest"; - - private static final Gson GSON = new Gson(); - - private static final Type RELEASE_TYPE = new TypeToken() { - }.getType(); - - public interface IUpdateCallback { - void onUpdate(Release r); - } - - private JadxUpdate() { - } - - public static void check(final IUpdateCallback callback) { - Runnable 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 IOException { - if (Jadx.isDevVersion()) { - LOG.debug("Ignore check for update: development version"); - return null; - } - Release latest = get(GITHUB_LATEST_RELEASE_URL, RELEASE_TYPE); - if (latest == null) { - return null; - } - String currentVersion = JadxDecompiler.getVersion(); - String latestName = latest.getName(); - if (latestName.equalsIgnoreCase(currentVersion)) { - return null; - } - if (VersionComparator.checkAndCompare(currentVersion, latestName) >= 0) { - return null; - } - LOG.info("Found new jadx version: {}", latest); - return latest; - } - - private static T get(String url, Type type) throws IOException { - URL obj = new URL(url); - HttpURLConnection con = (HttpURLConnection) obj.openConnection(); - con.setRequestMethod("GET"); - if (con.getResponseCode() == 200) { - Reader reader = new InputStreamReader(con.getInputStream(), StandardCharsets.UTF_8); - return GSON.fromJson(reader, type); - } - return null; - } -} diff --git a/jadx-gui/src/main/java/jadx/gui/update/JadxUpdate.kt b/jadx-gui/src/main/java/jadx/gui/update/JadxUpdate.kt new file mode 100644 index 000000000..e1dfafd1b --- /dev/null +++ b/jadx-gui/src/main/java/jadx/gui/update/JadxUpdate.kt @@ -0,0 +1,136 @@ +package jadx.gui.update + +import com.google.gson.Gson +import com.google.gson.JsonParser +import jadx.api.JadxDecompiler +import jadx.core.Jadx +import jadx.gui.settings.JadxUpdateChannel +import jadx.gui.update.data.Artifact +import jadx.gui.update.data.Release +import org.jetbrains.kotlin.konan.file.use +import org.slf4j.Logger +import org.slf4j.LoggerFactory +import java.io.InputStream +import java.io.InputStreamReader +import java.net.HttpURLConnection +import java.net.URL +import java.time.ZonedDateTime +import java.time.format.DateTimeFormatter +import java.util.Date + +object JadxUpdate { + private val LOG: Logger = LoggerFactory.getLogger(JadxUpdate::class.java) + + const val JADX_ARTIFACTS_URL = "https://nightly.link/skylot/jadx/workflows/build-artifacts/master" + const val JADX_RELEASES_URL = "https://github.com/skylot/jadx/releases" + + private const val GITHUB_API_URL = "https://api.github.com/repos/skylot/jadx" + private const val GITHUB_ARTIFACTS_URL = "$GITHUB_API_URL/actions/artifacts" + private const val GITHUB_LATEST_RELEASE_URL = "$GITHUB_API_URL/releases/latest" + + @JvmStatic + fun check(updateChannel: JadxUpdateChannel, callback: IUpdateCallback) { + Thread { + try { + val release = checkForNewRelease(updateChannel) + if (release != null) { + callback.onUpdate(release) + } + } catch (e: Exception) { + LOG.debug("Jadx update error", e) + } + }.apply { + name = "Jadx update thread" + priority = Thread.MIN_PRIORITY + start() + } + } + + private fun checkForNewRelease(updateChannel: JadxUpdateChannel): Release? { + if (Jadx.isDevVersion()) { + LOG.debug("Ignore check for update: development version") + return null + } + + LOG.info("Checking for updates... Update channel: {}, current version: {}", updateChannel, JadxDecompiler.getVersion()) + + return when (updateChannel) { + JadxUpdateChannel.STABLE -> checkForNewStableRelease() + JadxUpdateChannel.UNSTABLE -> checkForNewUnstableRelease() + } + } + + private fun checkForNewStableRelease(): Release? { + val latestRelease = get(GITHUB_LATEST_RELEASE_URL)?.let { inputStream -> + InputStreamReader(inputStream).use { + Gson().fromJson(it, Release::class.java) + } + } ?: return null + + val currentVersion = JadxDecompiler.getVersion() + + if (currentVersion.equals(latestRelease.name, ignoreCase = true)) return null + if (VersionComparator.checkAndCompare(currentVersion, latestRelease.name) >= 0) return null + + LOG.info("Found new jadx version: {}", latestRelease) + + return latestRelease + } + + private fun checkForNewUnstableRelease(): Release? { + val artifacts = getArtifacts() ?: return null + + val currentVersion = JadxDecompiler.getVersion() + val currentArtifactName = "jadx-$currentVersion" + + var newestArtifact: Artifact? = null + var currentArtifact: Artifact? = null + + for (artifact in artifacts) { + if (newestArtifact == null && artifact.name.startsWith("jadx-") && !artifact.name.startsWith("jadx-gui-")) { + newestArtifact = artifact + } + if (currentArtifact == null && artifact.name == currentArtifactName) { + currentArtifact = artifact + } + if (newestArtifact != null && currentArtifact != null) break + } + + LOG.debug("Current artifact: {}, newest artifact: {}", currentArtifact, newestArtifact) + + return if (currentArtifact != null && newestArtifact != null && newestArtifact.createdAt > currentArtifact.createdAt) { + newestArtifact.let { Release().apply { name = it.name } } + } else { + null + } + } + + private fun getArtifacts(): List? { + return get(GITHUB_ARTIFACTS_URL)?.let { inputStream -> + InputStreamReader(inputStream).use { reader -> + val response = JsonParser.parseReader(reader).asJsonObject + + val count = response.get("total_count").asInt + LOG.debug("Fetched $count artifacts...") + + response.getAsJsonArray("artifacts").map { + val obj = it.asJsonObject + val name = obj.get("name").asString + val sizeInBytes = obj.get("size_in_bytes").asLong + val createdAt = obj.get("created_at").asString + val parsedCreatedAt = ZonedDateTime.parse(createdAt, DateTimeFormatter.ISO_ZONED_DATE_TIME) + Artifact(name, sizeInBytes, Date.from(parsedCreatedAt.toInstant())) + } + } + } + } + + private fun get(url: String): InputStream? { + val con = URL(url).openConnection() as HttpURLConnection + return if (con.responseCode == 200) con.inputStream else null + } + + interface IUpdateCallback { + fun onUpdate(r: Release) + } +} diff --git a/jadx-gui/src/main/java/jadx/gui/update/data/Artifact.kt b/jadx-gui/src/main/java/jadx/gui/update/data/Artifact.kt new file mode 100644 index 000000000..57bac8d0d --- /dev/null +++ b/jadx-gui/src/main/java/jadx/gui/update/data/Artifact.kt @@ -0,0 +1,9 @@ +package jadx.gui.update.data + +import java.util.Date + +data class Artifact( + val name: String, + val sizeInBytes: Long, + val createdAt: Date, +) diff --git a/jadx-gui/src/main/java/jadx/gui/utils/Link.java b/jadx-gui/src/main/java/jadx/gui/utils/Link.java index a2c3dc5dc..08af7531c 100644 --- a/jadx-gui/src/main/java/jadx/gui/utils/Link.java +++ b/jadx-gui/src/main/java/jadx/gui/utils/Link.java @@ -20,13 +20,20 @@ public class Link extends JLabel { private static final Logger LOG = LoggerFactory.getLogger(Link.class); - private final String url; + private String url; + + public Link() { + super(); + init(); + } public Link(String text, String url) { super(text); - this.url = url; - setText(text); - setToolTipText("Open " + url + " in your browser"); + init(); + setUrl(url); + } + + private void init() { setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR)); this.addMouseListener(new MouseAdapter() { @Override @@ -36,6 +43,11 @@ public class Link extends JLabel { }); } + public void setUrl(String url) { + this.url = url; + setToolTipText("Open " + url + " in your browser"); + } + private void browse() { if (Desktop.isDesktopSupported()) { Desktop desktop = Desktop.getDesktop(); 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 48070b9ff..7e880ca58 100644 --- a/jadx-gui/src/main/resources/i18n/Messages_de_DE.properties +++ b/jadx-gui/src/main/resources/i18n/Messages_de_DE.properties @@ -205,6 +205,7 @@ preferences.excludedPackages.editDialog=Liste der durch Leerzeichen getren preferences.cfg=CFG-Grafiken für Methoden generieren (im 'dot'-Format) preferences.raw_cfg=RAW CFG-Grafiken generieren #preferences.xposed_codegen_language=Xposed code generation language +#preferences.update_channel=Jadx update channel #preferences.integerFormat=Integer format preferences.font=Schrift ändern #preferences.smali_font= 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 c809f875e..3ab35079e 100644 --- a/jadx-gui/src/main/resources/i18n/Messages_en_US.properties +++ b/jadx-gui/src/main/resources/i18n/Messages_en_US.properties @@ -205,6 +205,7 @@ preferences.excludedPackages.editDialog=List of space separated package na preferences.cfg=Generate methods CFG graphs (in 'dot' format) preferences.raw_cfg=Generate RAW CFG graphs preferences.xposed_codegen_language=Xposed code generation language +preferences.update_channel=Jadx update channel preferences.integerFormat=Integer format preferences.font=Editor font preferences.smali_font=Monospaced font (Smali/Hex) 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 7564db31b..d9717de4c 100644 --- a/jadx-gui/src/main/resources/i18n/Messages_es_ES.properties +++ b/jadx-gui/src/main/resources/i18n/Messages_es_ES.properties @@ -205,6 +205,7 @@ preferences.threads=Número de hilos a procesar preferences.cfg=Generar methods CFG graphs (in 'dot' format) preferences.raw_cfg=Generate RAW CFG graphs #preferences.xposed_codegen_language=Xposed code generation language +#preferences.update_channel=Jadx update channel #preferences.integerFormat=Integer format preferences.font=Fuente del editor #preferences.smali_font= diff --git a/jadx-gui/src/main/resources/i18n/Messages_id_ID.properties b/jadx-gui/src/main/resources/i18n/Messages_id_ID.properties index af15657f2..ac9763fae 100644 --- a/jadx-gui/src/main/resources/i18n/Messages_id_ID.properties +++ b/jadx-gui/src/main/resources/i18n/Messages_id_ID.properties @@ -205,6 +205,7 @@ preferences.excludedPackages.editDialog=Daftar nama paket yang dipisahkan preferences.cfg=Hasilkan grafik CFG metode (dalam format 'dot') preferences.raw_cfg=Hasilkan grafik CFG mentah #preferences.xposed_codegen_language=Xposed code generation language +#preferences.update_channel=Jadx update channel preferences.integerFormat=Format bilangan bulat preferences.font=Font editor preferences.smali_font=Font monospasi (Smali/Hex) 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 64079f72a..5b839ce14 100644 --- a/jadx-gui/src/main/resources/i18n/Messages_ko_KR.properties +++ b/jadx-gui/src/main/resources/i18n/Messages_ko_KR.properties @@ -205,6 +205,7 @@ preferences.excludedPackages.editDialog=RAM 절약을 위해 디컴파일 preferences.cfg=메소드 CFG 그래프 생성 ('dot' 포맷) preferences.raw_cfg=RAW CFG 그래프 생성 #preferences.xposed_codegen_language=Xposed code generation language +#preferences.update_channel=Jadx update channel #preferences.integerFormat=Integer format preferences.font=에디터 글씨체 #preferences.smali_font= 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 4bde44cb8..f674617b1 100644 --- a/jadx-gui/src/main/resources/i18n/Messages_pt_BR.properties +++ b/jadx-gui/src/main/resources/i18n/Messages_pt_BR.properties @@ -205,6 +205,7 @@ preferences.excludedPackages.editDialog=Lista espaço de pacotes que não preferences.cfg=Gera gráficos de métodos CFG no formato de pontos ('dot') preferences.raw_cfg=Gera gráficos CFG no formato RAW #preferences.xposed_codegen_language=Xposed code generation language +#preferences.update_channel=Jadx update channel #preferences.integerFormat=Integer format preferences.font=Fonte do editor #preferences.smali_font= 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 96dcb00f8..f498113a4 100644 --- a/jadx-gui/src/main/resources/i18n/Messages_ru_RU.properties +++ b/jadx-gui/src/main/resources/i18n/Messages_ru_RU.properties @@ -205,6 +205,7 @@ preferences.excludedPackages.editDialog=Список пакетов, ко preferences.cfg=Методы генерации графиков CFG (в "dot" формате) preferences.raw_cfg=Генерировать необработанные графики CFG #preferences.xposed_codegen_language=Xposed code generation language +#preferences.update_channel=Jadx update channel #preferences.integerFormat=Integer format preferences.font=Шрифт редактора Java #preferences.smali_font= 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 2d0d19812..f8c7b061b 100644 --- a/jadx-gui/src/main/resources/i18n/Messages_zh_CN.properties +++ b/jadx-gui/src/main/resources/i18n/Messages_zh_CN.properties @@ -205,6 +205,7 @@ preferences.excludedPackages.editDialog=排除于反编译或索引的以 preferences.cfg=生成方法的 CFG 图('.dot') preferences.raw_cfg=生成原始的 CFG 图 preferences.xposed_codegen_language=Xposed代码生成语言 +#preferences.update_channel=Jadx update channel preferences.integerFormat=数值格式化 preferences.font=编辑器字体 preferences.smali_font=等宽字体 (Smali/Hex) 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 0d65794a0..8deea592d 100644 --- a/jadx-gui/src/main/resources/i18n/Messages_zh_TW.properties +++ b/jadx-gui/src/main/resources/i18n/Messages_zh_TW.properties @@ -205,6 +205,7 @@ preferences.excludedPackages.editDialog=排除於索引或反編譯外的 preferences.cfg=產生方法 CFG 圖表 ('dot' 格式) preferences.raw_cfg=產生 RAW CFG 圖表 #preferences.xposed_codegen_language=Xposed code generation language +#preferences.update_channel=Jadx update channel preferences.integerFormat=整數模式 preferences.font=編輯器字型 preferences.smali_font=等寬字型 (Smali/Hex)