Switch update checker to GitHub Releases

- UpdateHelper now fetches latest release from GitHub API instead of helper bot
- Compares dotted version names, prefers arm64 APK asset, falls back to release page
- Keeps TL_help_appUpdate output contract so existing update UI works unchanged
- Decouples ConfigHelper from update check
This commit is contained in:
instant992 2026-06-09 03:00:30 +04:00
parent 42a780f702
commit 12792f77f3

View file

@ -1,27 +1,42 @@
package tw.nekomimi.nekogram.helpers.remote;
import android.content.pm.PackageInfo;
import android.os.Build;
import android.text.TextUtils;
import com.google.gson.annotations.Expose;
import com.google.gson.Gson;
import com.google.gson.annotations.SerializedName;
import org.telegram.messenger.AndroidUtilities;
import org.telegram.messenger.ApplicationLoader;
import org.telegram.messenger.BuildConfig;
import org.telegram.messenger.FileLog;
import org.telegram.messenger.LocaleController;
import org.telegram.messenger.R;
import org.telegram.messenger.Utilities;
import org.telegram.tgnet.TLRPC;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.stream.Collectors;
import java.util.List;
import java.util.regex.Pattern;
public class UpdateHelper extends BaseRemoteHelper {
public static final String UPDATE_METHOD = "check_for_updates";
// GitHub repository that hosts FoxiGram releases.
private static final String GITHUB_REPO = "instant992/FoxiGram";
private static final String RELEASES_API = "https://api.github.com/repos/" + GITHUB_REPO + "/releases/latest";
private static final Pattern VERSION_PART = Pattern.compile("\\d+");
private volatile boolean checking;
/**
* @param date {long} - date in milliseconds
*/
@ -80,7 +95,9 @@ public class UpdateHelper extends BaseRemoteHelper {
@Override
protected void onError(String text, Delegate delegate) {
delegate.onTLResponse(null, text);
if (delegate != null) {
delegate.onTLResponse(null, text);
}
}
@Override
@ -90,70 +107,194 @@ public class UpdateHelper extends BaseRemoteHelper {
@Override
protected String getRequestParams() {
return " " + TextUtils.join(",", Build.SUPPORTED_ABIS);
}
@Override
protected void onLoadSuccess(ArrayList<TLRPC.BotInlineResult> results, Delegate delegate) {
var map = results.stream()
.collect(Collectors.toMap(result -> result.id, result -> result));
var update_info = map.get("update_info");
if (update_info == null) {
delegate.onTLResponse(null, null);
return;
}
var update = new TLRPC.TL_help_appUpdate();
var json = GSON.fromJson(getTextFromInlineResult(update_info), Update.class);
if (json == null || json.versionCode <= BuildConfig.VERSION_CODE) {
delegate.onTLResponse(null, null);
return;
}
update.version = json.version;
update.can_not_skip = json.canNotSkip;
if (json.url != null) {
update.url = json.url;
update.flags |= 4;
}
var document = map.get("document");
if (document != null && document.document != null) {
update.document = document.document;
update.flags |= 2;
}
var message = map.get("message");
if (message != null && message.send_message != null) {
update.text = message.send_message.message;
update.entities = message.send_message.entities;
var entities = map.get("entities");
if (entities != null) {
var entities_json = GSON.fromJson(getTextFromInlineResult(entities), MessageEntity[].class);
update.entities.addAll(parseBotAPIEntities(entities_json, true));
}
}
var sticker = map.get("sticker");
if (sticker != null && sticker.document != null) {
update.sticker = sticker.document;
update.flags |= 8;
}
delegate.onTLResponse(update, null);
return "";
}
public void checkNewVersionAvailable(Delegate delegate) {
load(delegate);
ConfigHelper.getInstance().load();
if (checking) {
return;
}
checking = true;
Utilities.globalQueue.postRunnable(() -> fetchLatestRelease(delegate));
}
public static class Update {
@SerializedName("can_not_skip")
@Expose
public Boolean canNotSkip;
@SerializedName("version")
@Expose
public String version;
@SerializedName("version_code")
@Expose
public Integer versionCode;
@SerializedName("url")
@Expose
public String url;
private void fetchLatestRelease(Delegate delegate) {
HttpURLConnection connection = null;
try {
URL url = new URL(RELEASES_API);
connection = (HttpURLConnection) url.openConnection();
connection.setRequestMethod("GET");
connection.setRequestProperty("Accept", "application/vnd.github+json");
connection.setRequestProperty("User-Agent", ApplicationLoader.getApplicationId());
connection.setConnectTimeout(15000);
connection.setReadTimeout(15000);
int code = connection.getResponseCode();
if (code != HttpURLConnection.HTTP_OK) {
deliverError(delegate, "HTTP " + code);
return;
}
StringBuilder sb = new StringBuilder();
try (BufferedReader reader = new BufferedReader(new InputStreamReader(connection.getInputStream(), StandardCharsets.UTF_8))) {
String line;
while ((line = reader.readLine()) != null) {
sb.append(line);
}
}
GithubRelease release = new Gson().fromJson(sb.toString(), GithubRelease.class);
TLRPC.TL_help_appUpdate update = buildUpdate(release);
deliverSuccess(delegate, update);
} catch (Exception e) {
FileLog.e(e);
deliverError(delegate, e.getLocalizedMessage());
} finally {
if (connection != null) {
connection.disconnect();
}
checking = false;
}
}
private TLRPC.TL_help_appUpdate buildUpdate(GithubRelease release) {
if (release == null || release.draft || release.prerelease || TextUtils.isEmpty(release.tagName)) {
return null;
}
if (!isNewerVersion(release.tagName, BuildConfig.VERSION_NAME)) {
return null;
}
String apkUrl = selectApkAsset(release.assets);
if (apkUrl == null) {
// No installable asset attached; fall back to the release page.
apkUrl = release.htmlUrl;
}
if (TextUtils.isEmpty(apkUrl)) {
return null;
}
TLRPC.TL_help_appUpdate update = new TLRPC.TL_help_appUpdate();
update.version = stripVersionPrefix(release.tagName);
update.can_not_skip = false;
update.url = apkUrl;
update.flags |= 4;
String body = TextUtils.isEmpty(release.body) ? "" : release.body.trim();
String title = TextUtils.isEmpty(release.name) ? update.version : release.name.trim();
update.text = TextUtils.isEmpty(body) ? title : title + "\n\n" + body;
update.entities = new ArrayList<>();
return update;
}
private static String selectApkAsset(List<GithubAsset> assets) {
if (assets == null || assets.isEmpty()) {
return null;
}
String anyApk = null;
for (GithubAsset asset : assets) {
if (asset == null || TextUtils.isEmpty(asset.name) || TextUtils.isEmpty(asset.downloadUrl)) {
continue;
}
String name = asset.name.toLowerCase();
if (!name.endsWith(".apk")) {
continue;
}
// The build only targets arm64-v8a, prefer a matching asset.
if (name.contains("arm64")) {
return asset.downloadUrl;
}
if (anyApk == null) {
anyApk = asset.downloadUrl;
}
}
return anyApk;
}
private static String stripVersionPrefix(String tag) {
if (tag == null) {
return "";
}
String t = tag.trim();
if (t.startsWith("v") || t.startsWith("V")) {
t = t.substring(1);
}
return t;
}
// Compares dotted numeric versions (e.g. "12.7.4" > "12.7.3"). Non-numeric
// suffixes are ignored. Returns true when remote is strictly newer.
private static boolean isNewerVersion(String remoteTag, String currentName) {
int[] remote = parseVersion(stripVersionPrefix(remoteTag));
int[] current = parseVersion(currentName);
int len = Math.max(remote.length, current.length);
for (int i = 0; i < len; i++) {
int r = i < remote.length ? remote[i] : 0;
int c = i < current.length ? current[i] : 0;
if (r != c) {
return r > c;
}
}
return false;
}
private static int[] parseVersion(String version) {
if (TextUtils.isEmpty(version)) {
return new int[0];
}
String[] rawParts = version.split("\\.");
ArrayList<Integer> parts = new ArrayList<>();
for (String part : rawParts) {
var matcher = VERSION_PART.matcher(part);
if (matcher.find()) {
try {
parts.add(Integer.parseInt(matcher.group()));
} catch (NumberFormatException ignored) {
parts.add(0);
}
}
}
int[] result = new int[parts.size()];
for (int i = 0; i < result.length; i++) {
result[i] = parts.get(i);
}
return result;
}
private void deliverSuccess(Delegate delegate, TLRPC.TL_help_appUpdate update) {
if (delegate == null) {
return;
}
AndroidUtilities.runOnUIThread(() -> delegate.onTLResponse(update, null));
}
private void deliverError(Delegate delegate, String error) {
if (delegate == null) {
return;
}
AndroidUtilities.runOnUIThread(() -> delegate.onTLResponse(null, error));
}
private static class GithubRelease {
@SerializedName("tag_name")
String tagName;
@SerializedName("name")
String name;
@SerializedName("body")
String body;
@SerializedName("html_url")
String htmlUrl;
@SerializedName("draft")
boolean draft;
@SerializedName("prerelease")
boolean prerelease;
@SerializedName("assets")
List<GithubAsset> assets;
}
private static class GithubAsset {
@SerializedName("name")
String name;
@SerializedName("browser_download_url")
String downloadUrl;
@SerializedName("size")
long size;
}
}