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:
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user