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,159 +1,300 @@
package tw.nekomimi.nekogram.helpers.remote; package tw.nekomimi.nekogram.helpers.remote;
import android.content.pm.PackageInfo; import android.content.pm.PackageInfo;
import android.os.Build; import android.text.TextUtils;
import android.text.TextUtils;
import com.google.gson.Gson;
import com.google.gson.annotations.Expose; import com.google.gson.annotations.SerializedName;
import com.google.gson.annotations.SerializedName;
import org.telegram.messenger.AndroidUtilities;
import org.telegram.messenger.ApplicationLoader; import org.telegram.messenger.ApplicationLoader;
import org.telegram.messenger.BuildConfig; import org.telegram.messenger.BuildConfig;
import org.telegram.messenger.FileLog; import org.telegram.messenger.FileLog;
import org.telegram.messenger.LocaleController; import org.telegram.messenger.LocaleController;
import org.telegram.messenger.R; import org.telegram.messenger.R;
import org.telegram.tgnet.TLRPC; import org.telegram.messenger.Utilities;
import org.telegram.tgnet.TLRPC;
import java.util.ArrayList;
import java.util.Calendar; import java.io.BufferedReader;
import java.util.Date; import java.io.InputStreamReader;
import java.util.stream.Collectors; import java.net.HttpURLConnection;
import java.net.URL;
public class UpdateHelper extends BaseRemoteHelper { import java.nio.charset.StandardCharsets;
public static final String UPDATE_METHOD = "check_for_updates"; import java.util.ArrayList;
import java.util.Calendar;
/** import java.util.Date;
* @param date {long} - date in milliseconds import java.util.List;
*/ import java.util.regex.Pattern;
public static String formatDateUpdate(long date) {
long epoch; public class UpdateHelper extends BaseRemoteHelper {
try { public static final String UPDATE_METHOD = "check_for_updates";
PackageInfo pInfo = ApplicationLoader.applicationContext.getPackageManager().getPackageInfo(ApplicationLoader.applicationContext.getPackageName(), 0);
epoch = pInfo.lastUpdateTime; // GitHub repository that hosts FoxiGram releases.
} catch (Exception e) { private static final String GITHUB_REPO = "instant992/FoxiGram";
epoch = 0; private static final String RELEASES_API = "https://api.github.com/repos/" + GITHUB_REPO + "/releases/latest";
}
if (date <= epoch) { private static final Pattern VERSION_PART = Pattern.compile("\\d+");
return LocaleController.formatString(R.string.LastUpdateNever);
} private volatile boolean checking;
try {
Calendar rightNow = Calendar.getInstance(); /**
int day = rightNow.get(Calendar.DAY_OF_YEAR); * @param date {long} - date in milliseconds
int year = rightNow.get(Calendar.YEAR); */
rightNow.setTimeInMillis(date); public static String formatDateUpdate(long date) {
int dateDay = rightNow.get(Calendar.DAY_OF_YEAR); long epoch;
int dateYear = rightNow.get(Calendar.YEAR); try {
PackageInfo pInfo = ApplicationLoader.applicationContext.getPackageManager().getPackageInfo(ApplicationLoader.applicationContext.getPackageName(), 0);
if (dateDay == day && year == dateYear) { epoch = pInfo.lastUpdateTime;
if (Math.abs(System.currentTimeMillis() - date) < 60000L) { } catch (Exception e) {
return LocaleController.formatString(R.string.LastUpdateRecently); epoch = 0;
} }
return LocaleController.formatString(R.string.LastUpdateFormatted, LocaleController.formatString(R.string.TodayAtFormatted, if (date <= epoch) {
LocaleController.getInstance().getFormatterDay().format(new Date(date)))); return LocaleController.formatString(R.string.LastUpdateNever);
} else if (dateDay + 1 == day && year == dateYear) { }
return LocaleController.formatString(R.string.LastUpdateFormatted, LocaleController.formatString(R.string.YesterdayAtFormatted, try {
LocaleController.getInstance().getFormatterDay().format(new Date(date)))); Calendar rightNow = Calendar.getInstance();
} else if (Math.abs(System.currentTimeMillis() - date) < 31536000000L) { int day = rightNow.get(Calendar.DAY_OF_YEAR);
String format = LocaleController.formatString(R.string.formatDateAtTime, int year = rightNow.get(Calendar.YEAR);
LocaleController.getInstance().getFormatterDayMonth().format(new Date(date)), rightNow.setTimeInMillis(date);
LocaleController.getInstance().getFormatterDay().format(new Date(date))); int dateDay = rightNow.get(Calendar.DAY_OF_YEAR);
return LocaleController.formatString(R.string.LastUpdateDateFormatted, format); int dateYear = rightNow.get(Calendar.YEAR);
} else {
String format = LocaleController.formatString(R.string.formatDateAtTime, if (dateDay == day && year == dateYear) {
LocaleController.getInstance().getFormatterYear().format(new Date(date)), if (Math.abs(System.currentTimeMillis() - date) < 60000L) {
LocaleController.getInstance().getFormatterDay().format(new Date(date))); return LocaleController.formatString(R.string.LastUpdateRecently);
return LocaleController.formatString(R.string.LastUpdateDateFormatted, format); }
} return LocaleController.formatString(R.string.LastUpdateFormatted, LocaleController.formatString(R.string.TodayAtFormatted,
} catch (Exception e) { LocaleController.getInstance().getFormatterDay().format(new Date(date))));
FileLog.e(e); } else if (dateDay + 1 == day && year == dateYear) {
} return LocaleController.formatString(R.string.LastUpdateFormatted, LocaleController.formatString(R.string.YesterdayAtFormatted,
return "LOC_ERR"; LocaleController.getInstance().getFormatterDay().format(new Date(date))));
} } else if (Math.abs(System.currentTimeMillis() - date) < 31536000000L) {
String format = LocaleController.formatString(R.string.formatDateAtTime,
private static final class InstanceHolder { LocaleController.getInstance().getFormatterDayMonth().format(new Date(date)),
private static final UpdateHelper instance = new UpdateHelper(); LocaleController.getInstance().getFormatterDay().format(new Date(date)));
} return LocaleController.formatString(R.string.LastUpdateDateFormatted, format);
} else {
public static UpdateHelper getInstance() { String format = LocaleController.formatString(R.string.formatDateAtTime,
return InstanceHolder.instance; LocaleController.getInstance().getFormatterYear().format(new Date(date)),
} LocaleController.getInstance().getFormatterDay().format(new Date(date)));
return LocaleController.formatString(R.string.LastUpdateDateFormatted, format);
@Override }
protected void onError(String text, Delegate delegate) { } catch (Exception e) {
delegate.onTLResponse(null, text); FileLog.e(e);
} }
return "LOC_ERR";
@Override }
protected String getRequestMethod() {
return UPDATE_METHOD; private static final class InstanceHolder {
} private static final UpdateHelper instance = new UpdateHelper();
}
@Override
protected String getRequestParams() { public static UpdateHelper getInstance() {
return " " + TextUtils.join(",", Build.SUPPORTED_ABIS); return InstanceHolder.instance;
} }
@Override @Override
protected void onLoadSuccess(ArrayList<TLRPC.BotInlineResult> results, Delegate delegate) { protected void onError(String text, Delegate delegate) {
var map = results.stream() if (delegate != null) {
.collect(Collectors.toMap(result -> result.id, result -> result)); delegate.onTLResponse(null, text);
var update_info = map.get("update_info"); }
if (update_info == null) { }
delegate.onTLResponse(null, null);
return; @Override
} protected String getRequestMethod() {
var update = new TLRPC.TL_help_appUpdate(); return UPDATE_METHOD;
var json = GSON.fromJson(getTextFromInlineResult(update_info), Update.class); }
if (json == null || json.versionCode <= BuildConfig.VERSION_CODE) {
delegate.onTLResponse(null, null); @Override
return; protected String getRequestParams() {
} return "";
update.version = json.version; }
update.can_not_skip = json.canNotSkip;
if (json.url != null) { public void checkNewVersionAvailable(Delegate delegate) {
update.url = json.url; if (checking) {
update.flags |= 4; return;
} }
var document = map.get("document"); checking = true;
if (document != null && document.document != null) { Utilities.globalQueue.postRunnable(() -> fetchLatestRelease(delegate));
update.document = document.document; }
update.flags |= 2;
} private void fetchLatestRelease(Delegate delegate) {
var message = map.get("message"); HttpURLConnection connection = null;
if (message != null && message.send_message != null) { try {
update.text = message.send_message.message; URL url = new URL(RELEASES_API);
update.entities = message.send_message.entities; connection = (HttpURLConnection) url.openConnection();
var entities = map.get("entities"); connection.setRequestMethod("GET");
if (entities != null) { connection.setRequestProperty("Accept", "application/vnd.github+json");
var entities_json = GSON.fromJson(getTextFromInlineResult(entities), MessageEntity[].class); connection.setRequestProperty("User-Agent", ApplicationLoader.getApplicationId());
update.entities.addAll(parseBotAPIEntities(entities_json, true)); connection.setConnectTimeout(15000);
} connection.setReadTimeout(15000);
}
var sticker = map.get("sticker"); int code = connection.getResponseCode();
if (sticker != null && sticker.document != null) { if (code != HttpURLConnection.HTTP_OK) {
update.sticker = sticker.document; deliverError(delegate, "HTTP " + code);
update.flags |= 8; return;
} }
delegate.onTLResponse(update, null);
} StringBuilder sb = new StringBuilder();
try (BufferedReader reader = new BufferedReader(new InputStreamReader(connection.getInputStream(), StandardCharsets.UTF_8))) {
public void checkNewVersionAvailable(Delegate delegate) { String line;
load(delegate); while ((line = reader.readLine()) != null) {
ConfigHelper.getInstance().load(); sb.append(line);
} }
}
public static class Update {
@SerializedName("can_not_skip") GithubRelease release = new Gson().fromJson(sb.toString(), GithubRelease.class);
@Expose TLRPC.TL_help_appUpdate update = buildUpdate(release);
public Boolean canNotSkip; deliverSuccess(delegate, update);
@SerializedName("version") } catch (Exception e) {
@Expose FileLog.e(e);
public String version; deliverError(delegate, e.getLocalizedMessage());
@SerializedName("version_code") } finally {
@Expose if (connection != null) {
public Integer versionCode; connection.disconnect();
@SerializedName("url") }
@Expose checking = false;
public String url; }
} }
}
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;
}
}