feat(gui): allow to check for unstable releases (PR #2200)

* feat: gui: convert JadxUpdate to Kotlin

* feat: gui: allow updater to check for latest unstable artifacts

* fix: remove nullable operator from onUpdate() interface
This commit is contained in:
Iscle
2024-06-11 23:49:44 +02:00
committed by GitHub
parent 9aacb4f312
commit 74ddfde950
17 changed files with 212 additions and 106 deletions
@@ -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);
}
@@ -0,0 +1,6 @@
package jadx.gui.settings
enum class JadxUpdateChannel {
STABLE,
UNSTABLE,
}
@@ -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<JadxUpdateChannel> 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;
}
@@ -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();
@@ -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<Release>() {
}.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> 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;
}
}
@@ -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<Artifact>? {
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)
}
}
@@ -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,
)
@@ -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();
@@ -205,6 +205,7 @@ preferences.excludedPackages.editDialog=<html>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=
@@ -205,6 +205,7 @@ preferences.excludedPackages.editDialog=<html>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)
@@ -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=
@@ -205,6 +205,7 @@ preferences.excludedPackages.editDialog=<html>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)
@@ -205,6 +205,7 @@ preferences.excludedPackages.editDialog=<html>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=
@@ -205,6 +205,7 @@ preferences.excludedPackages.editDialog=<html>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=
@@ -205,6 +205,7 @@ preferences.excludedPackages.editDialog=<html>Список пакетов, ко
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=
@@ -205,6 +205,7 @@ preferences.excludedPackages.editDialog=<html>排除于反编译或索引的以
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)
@@ -205,6 +205,7 @@ preferences.excludedPackages.editDialog=<html>排除於索引或反編譯外的
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)